From 29212a31a7b5a53dec731a4c30252c05c89019f6 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Thu, 22 Aug 2013 16:43:36 +0200 Subject: [PATCH 0001/1122] Some additions to the user guide: getting started section + section on common numpy operations. --- doc/examples/plot_camera_numpy.py | 29 +++++++++ doc/source/user_guide.txt | 2 + doc/source/user_guide/getting_started.txt | 45 +++++++++++++ doc/source/user_guide/numpy_images.txt | 77 +++++++++++++++++++++++ 4 files changed, 153 insertions(+) create mode 100644 doc/examples/plot_camera_numpy.py create mode 100644 doc/source/user_guide/getting_started.txt create mode 100644 doc/source/user_guide/numpy_images.txt diff --git a/doc/examples/plot_camera_numpy.py b/doc/examples/plot_camera_numpy.py new file mode 100644 index 00000000..36d4be34 --- /dev/null +++ b/doc/examples/plot_camera_numpy.py @@ -0,0 +1,29 @@ +""" +Using simple NumPy operations for manipulating images +===================================================== + +This script illustrates how to use basic NumPy operations, such as slicing, +masking and fancy indexing, in order to modify the pixel values of an image. +""" + +import numpy as np +from skimage import data +import matplotlib.pyplot as plt + +camera = data.camera() +camera[:10] = 0 +mask = camera < 87 +camera[mask] = 255 +inds_x = np.arange(len(camera)) +inds_y = (4 * inds_x) % len(camera) +camera[inds_x, inds_y] = 0 + +l_x, l_y = camera.shape[0], camera.shape[1] +X, Y = np.ogrid[:l_x, :l_y] +outer_disk_mask = (X - l_x / 2)**2 + (Y - l_y / 2)**2 > (l_x / 2)**2 +camera[outer_disk_mask] = 0 + +plt.figure(figsize=(4, 4)) +plt.imshow(camera, cmap='gray', interpolation='nearest') +plt.axis('off') +plt.show() diff --git a/doc/source/user_guide.txt b/doc/source/user_guide.txt index 178e130d..338d9984 100644 --- a/doc/source/user_guide.txt +++ b/doc/source/user_guide.txt @@ -4,6 +4,8 @@ User Guide .. toctree:: :maxdepth: 2 + user_guide/getting_started + user_guide/numpy_images user_guide/data_types user_guide/plugins user_guide/tutorials diff --git a/doc/source/user_guide/getting_started.txt b/doc/source/user_guide/getting_started.txt new file mode 100644 index 00000000..1d480990 --- /dev/null +++ b/doc/source/user_guide/getting_started.txt @@ -0,0 +1,45 @@ +Getting started +--------------- + +``scikit-image`` is an image processing Python package that works with +:mod:`numpy` arrays. The package is imported as ``skimage``: + +:: + + >>> import skimage + +Most functions of ``skimage`` are found within submodules: :: + + >>> from skimage import data + >>> camera = data.camera() + +A list of submodules and functions is found on the `API reference +`_ webpage. + +Within ``scikit-image``, images are represented as NumPy arrays, for +example 2-D arrays for grayscale 2-D images :: + + >>> type(camera) + + >>> # An image with 512 rows and 512 columns + >>> camera.shape + (512, 512) + +The :mod:`skimage.data` submodule provides a set of functions returning +model images, that can be used to get started quickly on using +scikit-image's functions: :: + + >>> coins = data.coins() + >>> from skimage import filter + >>> threshold_value = filter.threshold_otsu(coins) + >>> threshold_value + 107 + +Of course, it is also possible to load your own images as NumPy arrays +from image files, using :func:`skimage.io.imread`: :: + + >>> import os + >>> filename = os.path.join(skimage.data_dir, 'moon.png') + >>> from skimage import io + >>> moon = io.imread(filename) + diff --git a/doc/source/user_guide/numpy_images.txt b/doc/source/user_guide/numpy_images.txt new file mode 100644 index 00000000..dd93db90 --- /dev/null +++ b/doc/source/user_guide/numpy_images.txt @@ -0,0 +1,77 @@ +A crash course on Numpy for images +---------------------------------- + +Images manipulated by ``scikit-image`` are simply NumPy arrays. Hence, a +large fraction of operations on images will just consist in using NumPy:: + + >>> from skimage import data + >>> camera = data.camera() + +Retrieving the geometry of the image and the number of pixels: :: + + >>> camera.shape + (512, 512) + >>> camera.size + 262144 + +Retrieving statistical information about gray values: :: + + >>> camera.min(), camera.max() + (0, 255) + >>> camera.mean() + 118.31400299072266 + +Numpy indexing can be used both for looking at pixel values, and to +modify pixel values: :: + + >>> # Value of pixel on 10th line and 20th column + >>> camera[10, 20] + 153 + >>> # Turn to black pixel on 3rd line and 10th column + >>> camera[3, 10] = 0 + +Be careful that the first dimension corresponds to lines, while the +second dimension (fastest varying dimension) stands for columns. + +Beyond individual pixels, it is possible to access / modify values of +whole sets of pixels, using the different indexing possibilities of +NumPy. + +Slicing:: + + >>> # Set to black the ten first lines + >>> camera[:10] = 0 + +Masking (indexing with masks of booleans):: + + >>> mask = camera < 87 + >>> # Set to "white" (255) pixels where mask is True + >>> camera[mask] = 255 + +Fancy indexing (indexing with sets of indices) :: + + >>> inds_x = np.arange(len(camera)) + >>> inds_y = 4 * inds_x % len(camera) + >>> camera[inds_x, inds_y] = 0 + +Using masks, especially, is very useful to select a set of pixels on +which to perform further manipulations. The mask can be any boolean array +of same shape as the image (or at least a shape broadcastable to the +image shape). This can be useful to define a region of interest, as a +disk: :: + + >>> l_x, l_y = camera.shape[0], camera.shape[1] + >>> X, Y = np.ogrid[:l_x, :l_y] + >>> outer_disk_mask = (X - l_x / 2)**2 + (Y - l_y / 2)**2 < (l_x / 2)**2 + >>> camera[outer_disk_mask] = 0 + +.. image:: ../../_images/plot_camera_numpy_1.png + :width: 45% + :target: ../auto_examples/plot_camera_numpy.html + +Boolean arithmetics can be used to define more complex masks: :: + + >>> lower_half = X > l_x / 2 + >>> lower_half_disk = np.logical_and(lower_half, outer_disk_mask) + >>> camera = data.camera() + >>> camera[lower_half_disk] = 0 From ebe1a1865647542cf984029cbd1bda4c6ec74c89 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sat, 24 Aug 2013 07:53:16 +0200 Subject: [PATCH 0002/1122] Minor modifications following review by Johannes --- doc/source/user_guide/getting_started.txt | 6 ++---- doc/source/user_guide/numpy_images.txt | 5 +++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/doc/source/user_guide/getting_started.txt b/doc/source/user_guide/getting_started.txt index 1d480990..21ea95a3 100644 --- a/doc/source/user_guide/getting_started.txt +++ b/doc/source/user_guide/getting_started.txt @@ -2,9 +2,7 @@ Getting started --------------- ``scikit-image`` is an image processing Python package that works with -:mod:`numpy` arrays. The package is imported as ``skimage``: - -:: +:mod:`numpy` arrays. The package is imported as ``skimage``: :: >>> import skimage @@ -16,7 +14,7 @@ Most functions of ``skimage`` are found within submodules: :: A list of submodules and functions is found on the `API reference `_ webpage. -Within ``scikit-image``, images are represented as NumPy arrays, for +Within scikit-image, images are represented as NumPy arrays, for example 2-D arrays for grayscale 2-D images :: >>> type(camera) diff --git a/doc/source/user_guide/numpy_images.txt b/doc/source/user_guide/numpy_images.txt index dd93db90..c03f703b 100644 --- a/doc/source/user_guide/numpy_images.txt +++ b/doc/source/user_guide/numpy_images.txt @@ -30,8 +30,9 @@ modify pixel values: :: >>> # Turn to black pixel on 3rd line and 10th column >>> camera[3, 10] = 0 -Be careful that the first dimension corresponds to lines, while the -second dimension (fastest varying dimension) stands for columns. +Be careful that the first dimension (``camera.shape[0]``) corresponds to +lines, while the second dimension (``camera.shape[1]``) stands for +columns. Beyond individual pixels, it is possible to access / modify values of whole sets of pixels, using the different indexing possibilities of From 7152d5c3140980be31c7671c470e4136694aeb55 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Thu, 8 May 2014 17:49:47 +0200 Subject: [PATCH 0003/1122] Move label to measure module (with deprecation). Warn that background parameter will default to 0 in v0.12. --- TODO.txt | 9 +++++++ doc/source/api_changes.txt | 1 - skimage/measure/__init__.py | 4 ++- skimage/morphology/__init__.py | 3 +-- skimage/morphology/ccomp.pyx | 26 +++++++++++++++----- skimage/morphology/tests/test_ccomp.py | 34 +++++++++++++++----------- 6 files changed, 53 insertions(+), 24 deletions(-) diff --git a/TODO.txt b/TODO.txt index bec8efe6..47d4ec90 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,3 +1,12 @@ +Remember to list any API changes below in `doc/source/api_changes.txt`. + +Version 0.12 +------------ +- Change `label` to mark background as 0, not -1, which is consistent with + SciPy's labelling. +- Remove `skimage.morphology.label` from `skimage.morphology.__init__`--it now + lives in `skimage.measure.label`. + Version 0.11 ------------ * Remove deprecated `reverse_map` parameter of `skimage.transform.warp` diff --git a/doc/source/api_changes.txt b/doc/source/api_changes.txt index 527c9fb0..0271bec0 100644 --- a/doc/source/api_changes.txt +++ b/doc/source/api_changes.txt @@ -24,4 +24,3 @@ Version 0.3 - Remove ``as_grey``, ``dtype`` keyword from ImageCollection - Remove ``dtype`` from imread - Generalise ImageCollection to accept a load_func - diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index d76e7a67..f7c0656a 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -8,6 +8,7 @@ from ._moments import moments, moments_central, moments_normalized, moments_hu from .profile import profile_line from .fit import LineModel, CircleModel, EllipseModel, ransac from .block import block_reduce +from ..morphology.ccomp import label __all__ = ['find_contours', @@ -28,4 +29,5 @@ __all__ = ['find_contours', 'marching_cubes', 'mesh_surface_area', 'correct_mesh_orientation', - 'profile_line'] + 'profile_line', + 'label'] diff --git a/skimage/morphology/__init__.py b/skimage/morphology/__init__.py index bee5ac13..5d2406e8 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -4,14 +4,13 @@ from .grey import (erosion, dilation, opening, closing, white_tophat, black_tophat) from .selem import (square, rectangle, diamond, disk, cube, octahedron, ball, octagon, star) -from .ccomp import label +from ._label import label from .watershed import watershed from ._skeletonize import skeletonize, medial_axis from .convex_hull import convex_hull_image, convex_hull_object from .greyreconstruct import reconstruction from .misc import remove_small_objects - __all__ = ['binary_erosion', 'binary_dilation', 'binary_opening', diff --git a/skimage/morphology/ccomp.pyx b/skimage/morphology/ccomp.pyx index 91e2611c..2d2f5b13 100644 --- a/skimage/morphology/ccomp.pyx +++ b/skimage/morphology/ccomp.pyx @@ -4,6 +4,7 @@ #cython: wraparound=False import numpy as np +import warnings cimport numpy as cnp @@ -82,7 +83,7 @@ cdef inline void link_bg(DTYPE_t *forest, DTYPE_t n, DTYPE_t *background_node): # Connected components search as described in Fiorio et al. -def label(input, DTYPE_t neighbors=8, DTYPE_t background=-1, return_num=False): +def label(input, DTYPE_t neighbors=8, background=None, return_num=False): """Label connected regions of an integer array. Two pixels are connected when they are neighbors and have the same value. @@ -104,7 +105,10 @@ def label(input, DTYPE_t neighbors=8, DTYPE_t background=-1, return_num=False): Whether to use 4- or 8-connectivity. background : int Consider all pixels with this value as background pixels, and label - them as -1. + them as -1. (Note: background pixels will be labeled as 0 starting with + version 0.12). + return_num : bool + Whether to return the number of assigned labels. Returns ------- @@ -157,17 +161,27 @@ def label(input, DTYPE_t neighbors=8, DTYPE_t background=-1, return_num=False): cdef DTYPE_t i, j + cdef DTYPE_t background_val + + if background is None: + background_val = -1 + warnings.warn(DeprecationWarning( + 'The default value for `background` will change to 0 in v0.12' + )) + else: + background_val = background + cdef DTYPE_t background_node = -999 if neighbors != 4 and neighbors != 8: raise ValueError('Neighbors must be either 4 or 8.') # Initialize the first row - if data[0, 0] == background: + if data[0, 0] == background_val: link_bg(forest_p, 0, &background_node) for j in range(1, cols): - if data[0, j] == background: + if data[0, j] == background_val: link_bg(forest_p, j, &background_node) if data[0, j] == data[0, j-1]: @@ -175,7 +189,7 @@ def label(input, DTYPE_t neighbors=8, DTYPE_t background=-1, return_num=False): for i in range(1, rows): # Handle the first column - if data[i, 0] == background: + if data[i, 0] == background_val: link_bg(forest_p, i * cols, &background_node) if data[i, 0] == data[i-1, 0]: @@ -186,7 +200,7 @@ def label(input, DTYPE_t neighbors=8, DTYPE_t background=-1, return_num=False): join_trees(forest_p, i*cols, (i-1)*cols + 1) for j in range(1, cols): - if data[i, j] == background: + if data[i, j] == background_val: link_bg(forest_p, i * cols + j, &background_node) if neighbors == 8: diff --git a/skimage/morphology/tests/test_ccomp.py b/skimage/morphology/tests/test_ccomp.py index 1169be85..34de8fb9 100644 --- a/skimage/morphology/tests/test_ccomp.py +++ b/skimage/morphology/tests/test_ccomp.py @@ -2,7 +2,7 @@ import numpy as np from numpy.testing import assert_array_equal, run_module_suite from skimage.morphology import label - +from warnings import catch_warnings class TestConnectedComponents: def setup(self): @@ -25,7 +25,9 @@ class TestConnectedComponents: def test_random(self): x = (np.random.random((20, 30)) * 5).astype(np.int) - labels = label(x) + with catch_warnings(DeprecationWarning): + labels = label(x) + n = labels.max() for i in range(n): values = x[labels == i] @@ -35,27 +37,29 @@ class TestConnectedComponents: x = np.array([[0, 0, 1], [0, 1, 0], [1, 0, 0]]) - assert_array_equal(label(x), - x) + with catch_warnings(DeprecationWarning): + assert_array_equal(label(x), x) def test_4_vs_8(self): x = np.array([[0, 1], [1, 0]], dtype=int) - assert_array_equal(label(x, 4), - [[0, 1], - [2, 3]]) - assert_array_equal(label(x, 8), - [[0, 1], - [1, 0]]) + with catch_warnings(DeprecationWarning): + assert_array_equal(label(x, 4), + [[0, 1], + [2, 3]]) + assert_array_equal(label(x, 8), + [[0, 1], + [1, 0]]) def test_background(self): x = np.array([[1, 0, 0], [1, 1, 5], [0, 0, 0]]) - assert_array_equal(label(x), [[0, 1, 1], - [0, 0, 2], - [3, 3, 3]]) + with catch_warnings(DeprecationWarning): + assert_array_equal(label(x), [[0, 1, 1], + [0, 0, 2], + [3, 3, 3]]) assert_array_equal(label(x, background=0), [[0, -1, -1], @@ -87,7 +91,9 @@ class TestConnectedComponents: [0, 0, 6], [5, 5, 5]]) - assert_array_equal(label(x, return_num=True)[1], 4) + with catch_warnings(DeprecationWarning): + assert_array_equal(label(x, return_num=True)[1], 4) + assert_array_equal(label(x, background=0, return_num=True)[1], 3) From 46829c6a33f37f850aa5a75f590a5f1cd61b67bb Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Thu, 8 May 2014 18:04:03 +0200 Subject: [PATCH 0004/1122] Move implementation of label to measure submodule --- skimage/measure/__init__.py | 2 +- .../ccomp.pxd => measure/_ccomp.pxd} | 0 .../ccomp.pyx => measure/_ccomp.pyx} | 0 skimage/measure/setup.py | 3 + skimage/morphology/_label.py | 71 +++++++++++++++++++ skimage/morphology/setup.py | 3 - skimage/segmentation/_felzenszwalb_cy.pyx | 2 +- 7 files changed, 76 insertions(+), 5 deletions(-) rename skimage/{morphology/ccomp.pxd => measure/_ccomp.pxd} (100%) rename skimage/{morphology/ccomp.pyx => measure/_ccomp.pyx} (100%) create mode 100644 skimage/morphology/_label.py diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index f7c0656a..ff8a83fc 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -8,7 +8,7 @@ from ._moments import moments, moments_central, moments_normalized, moments_hu from .profile import profile_line from .fit import LineModel, CircleModel, EllipseModel, ransac from .block import block_reduce -from ..morphology.ccomp import label +from ._ccomp import label __all__ = ['find_contours', diff --git a/skimage/morphology/ccomp.pxd b/skimage/measure/_ccomp.pxd similarity index 100% rename from skimage/morphology/ccomp.pxd rename to skimage/measure/_ccomp.pxd diff --git a/skimage/morphology/ccomp.pyx b/skimage/measure/_ccomp.pyx similarity index 100% rename from skimage/morphology/ccomp.pyx rename to skimage/measure/_ccomp.pyx diff --git a/skimage/measure/setup.py b/skimage/measure/setup.py index af8a4be1..0fab0787 100644 --- a/skimage/measure/setup.py +++ b/skimage/measure/setup.py @@ -12,10 +12,13 @@ def configuration(parent_package='', top_path=None): config = Configuration('measure', parent_package, top_path) config.add_data_dir('tests') + cython(['_ccomp.pyx'], working_path=base_path) cython(['_find_contours_cy.pyx'], working_path=base_path) cython(['_moments.pyx'], working_path=base_path) cython(['_marching_cubes_cy.pyx'], working_path=base_path) + config.add_extension('_ccomp', sources=['_ccomp.c'], + include_dirs=[get_numpy_include_dirs()]) config.add_extension('_find_contours_cy', sources=['_find_contours_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_moments', sources=['_moments.c'], diff --git a/skimage/morphology/_label.py b/skimage/morphology/_label.py new file mode 100644 index 00000000..0f027adf --- /dev/null +++ b/skimage/morphology/_label.py @@ -0,0 +1,71 @@ +__all__ = ['label'] + +from ..measure._ccomp import label as _label +from skimage._shared.utils import deprecated + +@deprecated('skimage.measure.label') +def label(input, neighbors=8, background=None, return_num=False): + """Label connected regions of an integer array. + + Two pixels are connected when they are neighbors and have the same value. + They can be neighbors either in a 4- or 8-connected sense:: + + 4-connectivity 8-connectivity + + [ ] [ ] [ ] [ ] + | \ | / + [ ]--[ ]--[ ] [ ]--[ ]--[ ] + | / | \\ + [ ] [ ] [ ] [ ] + + Parameters + ---------- + input : ndarray of dtype int + Image to label. + neighbors : {4, 8}, int + Whether to use 4- or 8-connectivity. + background : int + Consider all pixels with this value as background pixels, and label + them as -1. (Note: background pixels will be labeled as 0 starting with + version 0.12). + return_num : bool + Whether to return the number of assigned labels. + + Returns + ------- + labels : ndarray of dtype int + Labeled array, where all connected regions are assigned the + same integer value. + num : int, optional + Number of labels, which equals the maximum label index and is only + returned if return_num is `True`. + + Examples + -------- + >>> x = np.eye(3).astype(int) + >>> print(x) + [[1 0 0] + [0 1 0] + [0 0 1]] + + >>> print(m.label(x, neighbors=4)) + [[0 1 1] + [2 3 1] + [2 2 4]] + + >>> print(m.label(x, neighbors=8)) + [[0 1 1] + [1 0 1] + [1 1 0]] + + >>> x = np.array([[1, 0, 0], + ... [1, 1, 5], + ... [0, 0, 0]]) + + >>> print(m.label(x, background=0)) + [[ 0 -1 -1] + [ 0 0 1] + [-1 -1 -1]] + + """ + return _label(input, neighbors, background, return_num) diff --git a/skimage/morphology/setup.py b/skimage/morphology/setup.py index 1936377b..f0f69764 100644 --- a/skimage/morphology/setup.py +++ b/skimage/morphology/setup.py @@ -12,7 +12,6 @@ def configuration(parent_package='', top_path=None): config = Configuration('morphology', parent_package, top_path) config.add_data_dir('tests') - cython(['ccomp.pyx'], working_path=base_path) cython(['cmorph.pyx'], working_path=base_path) cython(['_watershed.pyx'], working_path=base_path) cython(['_skeletonize_cy.pyx'], working_path=base_path) @@ -20,8 +19,6 @@ def configuration(parent_package='', top_path=None): cython(['_convex_hull.pyx'], working_path=base_path) cython(['_greyreconstruct.pyx'], working_path=base_path) - config.add_extension('ccomp', sources=['ccomp.c'], - include_dirs=[get_numpy_include_dirs()]) config.add_extension('cmorph', sources=['cmorph.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_watershed', sources=['_watershed.c'], diff --git a/skimage/segmentation/_felzenszwalb_cy.pyx b/skimage/segmentation/_felzenszwalb_cy.pyx index c7961084..80afaedb 100644 --- a/skimage/segmentation/_felzenszwalb_cy.pyx +++ b/skimage/segmentation/_felzenszwalb_cy.pyx @@ -7,7 +7,7 @@ import scipy cimport cython cimport numpy as cnp -from skimage.morphology.ccomp cimport find_root, join_trees +from skimage.measure._ccomp cimport find_root, join_trees from ..util import img_as_float From 5ba3ea421c8beb09f9596bac174892e90f05baba Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Thu, 8 May 2014 19:14:38 +0200 Subject: [PATCH 0005/1122] Fix imports and suppress deprecation warnings --- skimage/measure/_regionprops.py | 5 +++-- skimage/morphology/convex_hull.py | 2 +- skimage/morphology/tests/test_ccomp.py | 11 ++++++----- skimage/transform/hough_transform.py | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 88f7f843..1cf23685 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -4,8 +4,9 @@ from math import sqrt, atan2, pi as PI import numpy as np from scipy import ndimage -from skimage.morphology import convex_hull_image, label -from skimage.measure import _moments +from skimage.morphology import convex_hull_image +from ._ccomp import label +from . import _moments __all__ = ['regionprops', 'perimeter'] diff --git a/skimage/morphology/convex_hull.py b/skimage/morphology/convex_hull.py index 4d54498d..f335fefd 100644 --- a/skimage/morphology/convex_hull.py +++ b/skimage/morphology/convex_hull.py @@ -3,7 +3,7 @@ __all__ = ['convex_hull_image', 'convex_hull_object'] import numpy as np from ._pnpoly import grid_points_inside_poly from ._convex_hull import possible_hull -from skimage.morphology import label +from ..measure._ccomp import label from skimage.util import unique_rows diff --git a/skimage/morphology/tests/test_ccomp.py b/skimage/morphology/tests/test_ccomp.py index 34de8fb9..2ca5577f 100644 --- a/skimage/morphology/tests/test_ccomp.py +++ b/skimage/morphology/tests/test_ccomp.py @@ -3,6 +3,7 @@ from numpy.testing import assert_array_equal, run_module_suite from skimage.morphology import label from warnings import catch_warnings +from _shared.util import skimage_deprecation class TestConnectedComponents: def setup(self): @@ -25,7 +26,7 @@ class TestConnectedComponents: def test_random(self): x = (np.random.random((20, 30)) * 5).astype(np.int) - with catch_warnings(DeprecationWarning): + with catch_warnings(skimage_deprecation): labels = label(x) n = labels.max() @@ -37,13 +38,13 @@ class TestConnectedComponents: x = np.array([[0, 0, 1], [0, 1, 0], [1, 0, 0]]) - with catch_warnings(DeprecationWarning): + with catch_warnings(skimage_deprecation): assert_array_equal(label(x), x) def test_4_vs_8(self): x = np.array([[0, 1], [1, 0]], dtype=int) - with catch_warnings(DeprecationWarning): + with catch_warnings(skimage_deprecation): assert_array_equal(label(x, 4), [[0, 1], [2, 3]]) @@ -56,7 +57,7 @@ class TestConnectedComponents: [1, 1, 5], [0, 0, 0]]) - with catch_warnings(DeprecationWarning): + with catch_warnings(skimage_deprecation): assert_array_equal(label(x), [[0, 1, 1], [0, 0, 2], [3, 3, 3]]) @@ -91,7 +92,7 @@ class TestConnectedComponents: [0, 0, 6], [5, 5, 5]]) - with catch_warnings(DeprecationWarning): + with catch_warnings(skimage_deprecation): assert_array_equal(label(x, return_num=True)[1], 4) assert_array_equal(label(x, background=0, return_num=True)[1], 3) diff --git a/skimage/transform/hough_transform.py b/skimage/transform/hough_transform.py index 35a9cea8..cbb4caa6 100644 --- a/skimage/transform/hough_transform.py +++ b/skimage/transform/hough_transform.py @@ -71,7 +71,7 @@ def hough_line_peaks(hspace, angles, dists, min_distance=9, min_angle=10, hspace *= mask hspace_t = hspace > threshold - label_hspace = morphology.label(hspace_t) + label_hspace = measure.label(hspace_t) props = measure.regionprops(label_hspace) coords = np.array([np.round(p.centroid) for p in props], dtype=int) From 898f0d7fb508bb39630bd9f7fb54ae8dd288a9b8 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Thu, 8 May 2014 19:31:50 +0200 Subject: [PATCH 0006/1122] Update bento build --- bento.info | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bento.info b/bento.info index f1cbabc1..d3dfd440 100644 --- a/bento.info +++ b/bento.info @@ -64,9 +64,9 @@ Library: Extension: skimage.filter._ctmf Sources: skimage/filter/_ctmf.pyx - Extension: skimage.morphology.ccomp + Extension: skimage.measure._ccomp Sources: - skimage/morphology/ccomp.pyx + skimage/measure/_ccomp.pyx Extension: skimage.morphology._watershed Sources: skimage/morphology/_watershed.pyx From 1de102a885679fc8bcf70dcd4a64f81987eb7d9a Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 9 May 2014 02:53:48 +0200 Subject: [PATCH 0007/1122] Fix broken and circular imports --- skimage/measure/_regionprops.py | 2 +- skimage/morphology/tests/test_ccomp.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 1cf23685..bbf627a0 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -4,7 +4,7 @@ from math import sqrt, atan2, pi as PI import numpy as np from scipy import ndimage -from skimage.morphology import convex_hull_image +from ..morphology.convex_hull import convex_hull_image from ._ccomp import label from . import _moments diff --git a/skimage/morphology/tests/test_ccomp.py b/skimage/morphology/tests/test_ccomp.py index 2ca5577f..69b4b9e5 100644 --- a/skimage/morphology/tests/test_ccomp.py +++ b/skimage/morphology/tests/test_ccomp.py @@ -3,7 +3,7 @@ from numpy.testing import assert_array_equal, run_module_suite from skimage.morphology import label from warnings import catch_warnings -from _shared.util import skimage_deprecation +from skimage._shared.utils import skimage_deprecation class TestConnectedComponents: def setup(self): From 4da4ec5b89f276c662156346207c7ca785c17e88 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 9 May 2014 03:00:28 +0200 Subject: [PATCH 0008/1122] Mention which parameters to label are optional. Simplify label deprecation. --- skimage/measure/_ccomp.pyx | 6 ++-- skimage/morphology/_label.py | 67 +----------------------------------- 2 files changed, 4 insertions(+), 69 deletions(-) diff --git a/skimage/measure/_ccomp.pyx b/skimage/measure/_ccomp.pyx index 2d2f5b13..26b9645a 100644 --- a/skimage/measure/_ccomp.pyx +++ b/skimage/measure/_ccomp.pyx @@ -101,13 +101,13 @@ def label(input, DTYPE_t neighbors=8, background=None, return_num=False): ---------- input : ndarray of dtype int Image to label. - neighbors : {4, 8}, int + neighbors : {4, 8}, int, optional Whether to use 4- or 8-connectivity. - background : int + background : int, optional Consider all pixels with this value as background pixels, and label them as -1. (Note: background pixels will be labeled as 0 starting with version 0.12). - return_num : bool + return_num : bool, optional Whether to return the number of assigned labels. Returns diff --git a/skimage/morphology/_label.py b/skimage/morphology/_label.py index 0f027adf..88312427 100644 --- a/skimage/morphology/_label.py +++ b/skimage/morphology/_label.py @@ -3,69 +3,4 @@ __all__ = ['label'] from ..measure._ccomp import label as _label from skimage._shared.utils import deprecated -@deprecated('skimage.measure.label') -def label(input, neighbors=8, background=None, return_num=False): - """Label connected regions of an integer array. - - Two pixels are connected when they are neighbors and have the same value. - They can be neighbors either in a 4- or 8-connected sense:: - - 4-connectivity 8-connectivity - - [ ] [ ] [ ] [ ] - | \ | / - [ ]--[ ]--[ ] [ ]--[ ]--[ ] - | / | \\ - [ ] [ ] [ ] [ ] - - Parameters - ---------- - input : ndarray of dtype int - Image to label. - neighbors : {4, 8}, int - Whether to use 4- or 8-connectivity. - background : int - Consider all pixels with this value as background pixels, and label - them as -1. (Note: background pixels will be labeled as 0 starting with - version 0.12). - return_num : bool - Whether to return the number of assigned labels. - - Returns - ------- - labels : ndarray of dtype int - Labeled array, where all connected regions are assigned the - same integer value. - num : int, optional - Number of labels, which equals the maximum label index and is only - returned if return_num is `True`. - - Examples - -------- - >>> x = np.eye(3).astype(int) - >>> print(x) - [[1 0 0] - [0 1 0] - [0 0 1]] - - >>> print(m.label(x, neighbors=4)) - [[0 1 1] - [2 3 1] - [2 2 4]] - - >>> print(m.label(x, neighbors=8)) - [[0 1 1] - [1 0 1] - [1 1 0]] - - >>> x = np.array([[1, 0, 0], - ... [1, 1, 5], - ... [0, 0, 0]]) - - >>> print(m.label(x, background=0)) - [[ 0 -1 -1] - [ 0 0 1] - [-1 -1 -1]] - - """ - return _label(input, neighbors, background, return_num) +label = deprecated('skimage.measure.label')(_label) From 135965fbe4d991eb0ceb2e903af7335f0441618c Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sun, 25 May 2014 20:05:17 +0200 Subject: [PATCH 0009/1122] Avoid circular imports --- skimage/measure/__init__.py | 2 +- skimage/measure/_label.py | 6 ++++++ skimage/measure/_regionprops.py | 4 ++-- skimage/morphology/__init__.py | 6 +++++- skimage/morphology/_label.py | 6 ------ skimage/morphology/convex_hull.py | 2 +- 6 files changed, 15 insertions(+), 11 deletions(-) create mode 100644 skimage/measure/_label.py delete mode 100644 skimage/morphology/_label.py diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index ff8a83fc..e07b9789 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -8,7 +8,7 @@ from ._moments import moments, moments_central, moments_normalized, moments_hu from .profile import profile_line from .fit import LineModel, CircleModel, EllipseModel, ransac from .block import block_reduce -from ._ccomp import label +from ._label import label __all__ = ['find_contours', diff --git a/skimage/measure/_label.py b/skimage/measure/_label.py new file mode 100644 index 00000000..47e8af74 --- /dev/null +++ b/skimage/measure/_label.py @@ -0,0 +1,6 @@ +from ._ccomp import label as _label + +def label(input, neighbors=8, background=None, return_num=False): + return _label(input, neighbors, background, return_num) + +label.__doc__ = _label.__doc__ diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index bbf627a0..848aa3a3 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -4,8 +4,7 @@ from math import sqrt, atan2, pi as PI import numpy as np from scipy import ndimage -from ..morphology.convex_hull import convex_hull_image -from ._ccomp import label +from ._label import label from . import _moments @@ -136,6 +135,7 @@ class _RegionProperties(object): @_cached_property def convex_image(self): + from ..morphology.convex_hull import convex_hull_image return convex_hull_image(self.image) @_cached_property diff --git a/skimage/morphology/__init__.py b/skimage/morphology/__init__.py index 5d2406e8..5b659093 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -4,13 +4,17 @@ from .grey import (erosion, dilation, opening, closing, white_tophat, black_tophat) from .selem import (square, rectangle, diamond, disk, cube, octahedron, ball, octagon, star) -from ._label import label from .watershed import watershed from ._skeletonize import skeletonize, medial_axis from .convex_hull import convex_hull_image, convex_hull_object from .greyreconstruct import reconstruction from .misc import remove_small_objects +from ..measure._label import label +from skimage._shared.utils import deprecated as _deprecated +label = _deprecated('skimage.measure.label')(label) + + __all__ = ['binary_erosion', 'binary_dilation', 'binary_opening', diff --git a/skimage/morphology/_label.py b/skimage/morphology/_label.py deleted file mode 100644 index 88312427..00000000 --- a/skimage/morphology/_label.py +++ /dev/null @@ -1,6 +0,0 @@ -__all__ = ['label'] - -from ..measure._ccomp import label as _label -from skimage._shared.utils import deprecated - -label = deprecated('skimage.measure.label')(_label) diff --git a/skimage/morphology/convex_hull.py b/skimage/morphology/convex_hull.py index f335fefd..c5b9eb3f 100644 --- a/skimage/morphology/convex_hull.py +++ b/skimage/morphology/convex_hull.py @@ -3,7 +3,7 @@ __all__ = ['convex_hull_image', 'convex_hull_object'] import numpy as np from ._pnpoly import grid_points_inside_poly from ._convex_hull import possible_hull -from ..measure._ccomp import label +from ..measure._label import label from skimage.util import unique_rows From c0be2ec505cd6c2150372a02403e543a9030f924 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 26 May 2014 01:21:58 +0200 Subject: [PATCH 0010/1122] Correctly apply catch_warnings() --- skimage/morphology/tests/test_ccomp.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/skimage/morphology/tests/test_ccomp.py b/skimage/morphology/tests/test_ccomp.py index 69b4b9e5..e934a5b7 100644 --- a/skimage/morphology/tests/test_ccomp.py +++ b/skimage/morphology/tests/test_ccomp.py @@ -26,7 +26,7 @@ class TestConnectedComponents: def test_random(self): x = (np.random.random((20, 30)) * 5).astype(np.int) - with catch_warnings(skimage_deprecation): + with catch_warnings(): labels = label(x) n = labels.max() @@ -38,13 +38,13 @@ class TestConnectedComponents: x = np.array([[0, 0, 1], [0, 1, 0], [1, 0, 0]]) - with catch_warnings(skimage_deprecation): + with catch_warnings(): assert_array_equal(label(x), x) def test_4_vs_8(self): x = np.array([[0, 1], [1, 0]], dtype=int) - with catch_warnings(skimage_deprecation): + with catch_warnings(): assert_array_equal(label(x, 4), [[0, 1], [2, 3]]) @@ -57,7 +57,7 @@ class TestConnectedComponents: [1, 1, 5], [0, 0, 0]]) - with catch_warnings(skimage_deprecation): + with catch_warnings(): assert_array_equal(label(x), [[0, 1, 1], [0, 0, 2], [3, 3, 3]]) @@ -92,7 +92,7 @@ class TestConnectedComponents: [0, 0, 6], [5, 5, 5]]) - with catch_warnings(skimage_deprecation): + with catch_warnings(): assert_array_equal(label(x, return_num=True)[1], 4) assert_array_equal(label(x, background=0, return_num=True)[1], 3) From a06956430d73d09c4833fb4d8b7e05c27ca4a3e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 27 May 2014 20:39:51 -0400 Subject: [PATCH 0011/1122] Move removal of deprated functionality by one version --- TODO.txt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/TODO.txt b/TODO.txt index 47d4ec90..adc70f48 100644 --- a/TODO.txt +++ b/TODO.txt @@ -2,15 +2,12 @@ Remember to list any API changes below in `doc/source/api_changes.txt`. Version 0.12 ------------ -- Change `label` to mark background as 0, not -1, which is consistent with +* Change `label` to mark background as 0, not -1, which is consistent with SciPy's labelling. -- Remove `skimage.morphology.label` from `skimage.morphology.__init__`--it now +* Remove `skimage.morphology.label` from `skimage.morphology.__init__`--it now lives in `skimage.measure.label`. - -Version 0.11 ------------- * Remove deprecated `reverse_map` parameter of `skimage.transform.warp` -* Change depecrated `enforce_connectivity=False`on skimage.segmentation.slic +* Change depecrated `enforce_connectivity=False` on skimage.segmentation.slic and set it to True as default * Remove deprecated `skimage.measure.fit.BaseModel._params` attribute * Remove deprecated `skimage.measure.fit.BaseModel._params`, From 24be78e433a05582948afbb84a040708ac48bb82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 27 May 2014 20:44:52 -0400 Subject: [PATCH 0012/1122] Set version 0.10 --- bento.info | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bento.info b/bento.info index d3dfd440..18310623 100644 --- a/bento.info +++ b/bento.info @@ -1,5 +1,5 @@ Name: scikit-image -Version: 0.10.dev0 +Version: 0.10.0 Summary: Image processing routines for SciPy Url: http://scikit-image.org DownloadUrl: http://github.com/scikit-image/scikit-image diff --git a/setup.py b/setup.py index 6a476c21..68cbec22 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ MAINTAINER_EMAIL = 'stefan@sun.ac.za' URL = 'http://scikit-image.org' LICENSE = 'Modified BSD' DOWNLOAD_URL = 'http://github.com/scikit-image/scikit-image' -VERSION = '0.10dev' +VERSION = '0.10.0' PYTHON_VERSION = (2, 5) DEPENDENCIES = { 'numpy': (1, 6), From a421953d81b3e5b37bcaec224add25a73a195e44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 27 May 2014 20:45:54 -0400 Subject: [PATCH 0013/1122] Add 0.10 to docs --- doc/source/_static/docversions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/_static/docversions.js b/doc/source/_static/docversions.js index b4fd531f..cb0d3339 100644 --- a/doc/source/_static/docversions.js +++ b/doc/source/_static/docversions.js @@ -1,4 +1,4 @@ -var versions = ['dev', '0.9.x', '0.8.0', '0.7.0', '0.6', '0.5', '0.4', '0.3']; +var versions = ['dev', '0.10.x', '0.9.x', '0.8.0', '0.7.0', '0.6', '0.5', '0.4', '0.3']; function insert_version_links() { for (i = 0; i < versions.length; i++){ From 2171f9404810c5f3e7167a9570a19a7206f80770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 27 May 2014 21:15:15 -0400 Subject: [PATCH 0014/1122] Change to http git protocol --- doc/gh-pages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/gh-pages.py b/doc/gh-pages.py index 75c8d9e3..7bcf802c 100755 --- a/doc/gh-pages.py +++ b/doc/gh-pages.py @@ -30,7 +30,7 @@ from subprocess import Popen, PIPE, CalledProcessError, check_call pages_dir = 'gh-pages' html_dir = 'build/html' pdf_dir = 'build/latex' -pages_repo = 'git@github.com:scikit-image/docs.git' +pages_repo = 'https://github.com/scikit-image/docs.git' #----------------------------------------------------------------------------- # Functions From ea6c9b27abe0595064b3517a24d0507ae23af2a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 27 May 2014 21:19:01 -0400 Subject: [PATCH 0015/1122] Update release instructions for building docs --- RELEASE.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASE.txt b/RELEASE.txt index b339bb51..36c3acf5 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -13,8 +13,8 @@ How to make a new release of ``skimage`` - Update the docs: - Edit ``doc/source/_static/docversions.js`` and commit - - Build a clean version of the docs. Run ``make`` in the root dir, then - ``rm -rf build; make html`` in the docs. + - Build a clean version of the docs. Run ``python setup.py install`` in the + root dir, then ``rm -rf build; make html`` in the docs. - Run ``make html`` again to copy the newly generated ``random.js`` into place. Double check ``random.js``, otherwise the skimage.org front page gets broken! From fb4729d60d193133a983c2d51056b23ad5b9fd32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 27 May 2014 21:45:45 -0400 Subject: [PATCH 0016/1122] Fix gh-pages build script --- doc/gh-pages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/gh-pages.py b/doc/gh-pages.py index 7bcf802c..80020b66 100755 --- a/doc/gh-pages.py +++ b/doc/gh-pages.py @@ -117,7 +117,7 @@ if __name__ == '__main__': try: cd(pages_dir) status = sh2('git status | head -1') - branch = re.match('\# On branch (.*)$', status).group(1) + branch = re.match('On branch (.*)$', status).group(1) if branch != 'gh-pages': e = 'On %r, git branch is %r, MUST be "gh-pages"' % (pages_dir, branch) From c026b21a6620fe9b94970819d96068eb86c6fe00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 27 May 2014 21:46:02 -0400 Subject: [PATCH 0017/1122] Increase version to 0.11dev --- bento.info | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bento.info b/bento.info index 18310623..227e51e8 100644 --- a/bento.info +++ b/bento.info @@ -1,5 +1,5 @@ Name: scikit-image -Version: 0.10.0 +Version: 0.11dev0 Summary: Image processing routines for SciPy Url: http://scikit-image.org DownloadUrl: http://github.com/scikit-image/scikit-image diff --git a/setup.py b/setup.py index 68cbec22..c13ac28c 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ MAINTAINER_EMAIL = 'stefan@sun.ac.za' URL = 'http://scikit-image.org' LICENSE = 'Modified BSD' DOWNLOAD_URL = 'http://github.com/scikit-image/scikit-image' -VERSION = '0.10.0' +VERSION = '0.11dev' PYTHON_VERSION = (2, 5) DEPENDENCIES = { 'numpy': (1, 6), From 7d50cb792699247e3be81e3be2d360d61e0d3fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 27 May 2014 21:59:25 -0400 Subject: [PATCH 0018/1122] Remove deprecated release instruction --- RELEASE.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/RELEASE.txt b/RELEASE.txt index 36c3acf5..b56b5bb8 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -45,8 +45,6 @@ How to make a new release of ``skimage`` - Sync your branch with the remote repo: ``git pull``. If you try to ``make gh-pages`` when your branch is out of sync, it creates headaches. - - Update stable and development version numbers in - ``_templates/sidebar_versions.html``. - Add release date to ``index.rst`` under "Announcements". - Add previous stable version documentation path to disallowed paths in `robots.txt` From 9b0b9c13d0f675fad50e3ff79558e9b39f10db26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 27 May 2014 22:39:22 -0400 Subject: [PATCH 0019/1122] Count number of commits --- doc/release/contribs.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/release/contribs.py b/doc/release/contribs.py index 58cb11d2..c0a34e6e 100755 --- a/doc/release/contribs.py +++ b/doc/release/contribs.py @@ -14,7 +14,7 @@ def call(cmd): return subprocess.check_output(shlex.split(cmd), universal_newlines=True).split('\n') tag_date = call("git show --format='%%ci' %s" % tag)[0] -print("Release %s was on %s" % (tag, tag_date)) +print("Release %s was on %s\n" % (tag, tag_date)) merges = call("git log --since='%s' --merges --format='>>>%%B' --reverse" % tag_date) merges = [m for m in merges if m.strip()] @@ -22,7 +22,10 @@ merges = '\n'.join(merges).split('>>>') merges = [m.split('\n')[:2] for m in merges] merges = [m for m in merges if len(m) == 2 and m[1].strip()] -print("\nIt contained the following %d merges:\n" % len(merges)) +num_commits = call("git rev-list %s..HEAD --count" % tag)[0] +print("A total of %s changes have been committed.\n" % num_commits) + +print("It contained the following %d merges:\n" % len(merges)) for (merge, message) in merges: if merge.startswith('Merge pull request #'): PR = ' (%s)' % merge.split()[3] From 8fcbc7615fce9c0ca507e24d770ec0fde8779e59 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sun, 1 Jun 2014 12:38:57 +0200 Subject: [PATCH 0020/1122] Fix markup in DEPENDS --- DEPENDS.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPENDS.txt b/DEPENDS.txt index c4cc8679..152e935b 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -47,7 +47,7 @@ functionality is only available with the following installed: walker segmentation. * `Pillow `__ - (or <`PIL http://www.pythonware.com/products/pil/>`__) + (or `PIL `__) The ``Pillow`` library (or equivalently ``PIL``) is used for Input/Output. * `Astropy `__ is required to use the FITS io plug-in. From ba55d3259bc5f1293a19ee67a4f26d4ae3213e2f Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 2 Jun 2014 12:41:39 +0200 Subject: [PATCH 0021/1122] Update MANIFEST to include all test data --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 4f6786be..1a059991 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,7 +3,7 @@ include setup*.py include MANIFEST.in include *.txt include Makefile -recursive-include skimage *.pyx *.pxd *.pxi *.py *.c *.h *.ini *.md5 +recursive-include skimage *.pyx *.pxd *.pxi *.py *.c *.h *.ini *.md5 *.npy *.txt recursive-include skimage/data * include doc/Makefile From 8cba840a057b6379c8def32ff57484af62855b75 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 11 Jun 2014 20:28:54 +1000 Subject: [PATCH 0022/1122] Clarify `mask` argument in `medial_axis` docstring --- skimage/morphology/_skeletonize.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/skimage/morphology/_skeletonize.py b/skimage/morphology/_skeletonize.py index ddc62008..edb250ff 100644 --- a/skimage/morphology/_skeletonize.py +++ b/skimage/morphology/_skeletonize.py @@ -164,11 +164,11 @@ def medial_axis(image, mask=None, return_distance=False): Parameters ---------- - image : binary ndarray + image : binary ndarray, shape (M, N) - mask : binary ndarray, optional - If a mask is given, only those elements with a true value in `mask` - are used for computing the medial axis. + mask : binary ndarray, shape (M, N), optional + If a mask is given, only those elements in `image` with a true + value in `mask` are used for computing the medial axis. return_distance : bool, optional If true, the distance transform is returned as well as the skeleton. @@ -179,7 +179,7 @@ def medial_axis(image, mask=None, return_distance=False): out : ndarray of bools Medial axis transform of the image - dist : ndarray of ints + dist : ndarray of ints, optional Distance transform of the image (only returned if `return_distance` is True) From 34cf1f9243da3d5efe9bd2a0435da2fcf0e4fa65 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 12 Jun 2014 01:20:53 +1000 Subject: [PATCH 0023/1122] Update parameter list in docstring for consistency --- skimage/morphology/_skeletonize.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/skimage/morphology/_skeletonize.py b/skimage/morphology/_skeletonize.py index edb250ff..d310bad3 100644 --- a/skimage/morphology/_skeletonize.py +++ b/skimage/morphology/_skeletonize.py @@ -163,22 +163,18 @@ def medial_axis(image, mask=None, return_distance=False): Parameters ---------- - image : binary ndarray, shape (M, N) - + The image of the shape to be skeletonized. mask : binary ndarray, shape (M, N), optional If a mask is given, only those elements in `image` with a true value in `mask` are used for computing the medial axis. - return_distance : bool, optional If true, the distance transform is returned as well as the skeleton. Returns ------- - out : ndarray of bools Medial axis transform of the image - dist : ndarray of ints, optional Distance transform of the image (only returned if `return_distance` is True) From 9446f6b71a740b8cd773dd33a62981d0066b8642 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sun, 15 Jun 2014 15:03:46 +0200 Subject: [PATCH 0024/1122] Use more recent NoNorm API --- skimage/viewer/canvastools/painttool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/viewer/canvastools/painttool.py b/skimage/viewer/canvastools/painttool.py index cc5f9705..66cec08d 100644 --- a/skimage/viewer/canvastools/painttool.py +++ b/skimage/viewer/canvastools/painttool.py @@ -102,7 +102,7 @@ class PaintTool(CanvasToolBase): self._overlay_plot = None elif self._overlay_plot is None: props = dict(cmap=self.cmap, alpha=self.alpha, - norm=mcolors.no_norm(), animated=True) + norm=mcolors.NoNorm(), animated=True) self._overlay_plot = self.ax.imshow(image, **props) else: self._overlay_plot.set_data(image) From dead8612c76122b1c3033a75df6d40427c2a17a7 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sun, 15 Jun 2014 15:04:40 +0200 Subject: [PATCH 0025/1122] Correctly compare mask to None --- skimage/morphology/watershed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/morphology/watershed.py b/skimage/morphology/watershed.py index 7318ceea..977223a0 100644 --- a/skimage/morphology/watershed.py +++ b/skimage/morphology/watershed.py @@ -211,7 +211,7 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): # If nothing is labeled, the output is empty and we don't have to # do anything c_output = c_output.flatten() - if c_mask == None: + if c_mask is None: c_mask = np.ones(c_image.shape, np.int8).flatten() else: c_mask = c_mask.astype(np.int8).flatten() From 56f66fb8b33397ff036709c4e4d30d19a32fb84c Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sun, 15 Jun 2014 15:07:22 +0200 Subject: [PATCH 0026/1122] Ignore auto-generated notebooks in Git --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2623ea74..1026f756 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ skimage/version.py .coverage doc/source/auto_examples/*.py doc/source/auto_examples/*.txt +doc/source/auto_examples/notebook doc/source/auto_examples/images/plot_*.png doc/source/auto_examples/images/thumb doc/source/auto_examples/applications/ From 39bf45a9f39951a7f15722575a783ce6304482b4 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sun, 15 Jun 2014 15:23:40 +0200 Subject: [PATCH 0027/1122] Fix median filter example --- viewer_examples/plugins/median_filter.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/viewer_examples/plugins/median_filter.py b/viewer_examples/plugins/median_filter.py index 36593c3d..beb313eb 100644 --- a/viewer_examples/plugins/median_filter.py +++ b/viewer_examples/plugins/median_filter.py @@ -1,10 +1,13 @@ from skimage import data -from skimage.filter import median_filter +from skimage.filter.rank import median +from skimage.morphology import disk from skimage.viewer import ImageViewer from skimage.viewer.widgets import Slider, OKCancelButtons, SaveButtons from skimage.viewer.plugins.base import Plugin +def median_filter(image, radius): + return median(image, selem=disk(radius)) image = data.coins() viewer = ImageViewer(image) From 00009fe9627b947e3bb0674a3a70c61f4be59fb6 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sun, 15 Jun 2014 23:47:16 +0200 Subject: [PATCH 0028/1122] Use same order as matplotlib for PySize/PyQT --- skimage/viewer/qt/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/viewer/qt/__init__.py b/skimage/viewer/qt/__init__.py index 8e7ab939..2fc4e946 100644 --- a/skimage/viewer/qt/__init__.py +++ b/skimage/viewer/qt/__init__.py @@ -5,12 +5,12 @@ qt_api = os.environ.get('QT_API') if qt_api is None: try: - import PySide - qt_api = 'pyside' + import PyQt4 + qt_api = 'pyqt' except ImportError: try: - import PyQt4 - qt_api = 'pyqt' + import PySide + qt_api = 'pyside' except ImportError: qt_api = None # Note that we don't want to raise an error because that would From 1eae917af3d68fc0e52c9a2ce92c09836b920d66 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 17 Jun 2014 17:00:42 +1000 Subject: [PATCH 0029/1122] Update label2rgb doc --- skimage/color/colorlabel.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/skimage/color/colorlabel.py b/skimage/color/colorlabel.py index ed74f3a8..87da724a 100644 --- a/skimage/color/colorlabel.py +++ b/skimage/color/colorlabel.py @@ -69,23 +69,29 @@ def label2rgb(label, image=None, colors=None, alpha=0.3, Parameters ---------- - label : array + label : array, shape (M, N) Integer array of labels with the same shape as `image`. - image : array + image : array, shape (M, N, 3), optional Image used as underlay for labels. If the input is an RGB image, it's converted to grayscale before coloring. - colors : list + colors : list, optional List of colors. If the number of labels exceeds the number of colors, then the colors are cycled. - alpha : float [0, 1] + alpha : float [0, 1], optional Opacity of colorized labels. Ignored if image is `None`. - bg_label : int + bg_label : int, optional Label that's treated as the background. - bg_color : str or array + bg_color : str or array, optional Background color. Must be a name in `color_dict` or RGB float values between [0, 1]. - image_alpha : float [0, 1] + image_alpha : float [0, 1], optional Opacity of the image. + + Returns + ------- + result : array of float, shape (M, N, 3) + The result of blending a cycling colormap (`colors`) for each distinct + value in `label` with the image, at a certain alpha value. """ if colors is None: colors = DEFAULT_COLORS From a1fd895c4a728e0c5b847169c7a06c9c73c66f4d Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 17 Jun 2014 17:45:15 +1000 Subject: [PATCH 0030/1122] Split label2rgb into two related functions --- skimage/color/colorlabel.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/skimage/color/colorlabel.py b/skimage/color/colorlabel.py index 87da724a..47cb0de9 100644 --- a/skimage/color/colorlabel.py +++ b/skimage/color/colorlabel.py @@ -64,7 +64,7 @@ def _match_label_with_color(label, colors, bg_label, bg_color): def label2rgb(label, image=None, colors=None, alpha=0.3, - bg_label=-1, bg_color=None, image_alpha=1): + bg_label=-1, bg_color=None, image_alpha=1, kind='overlay'): """Return an RGB image where color-coded labels are painted over the image. Parameters @@ -86,6 +86,11 @@ def label2rgb(label, image=None, colors=None, alpha=0.3, between [0, 1]. image_alpha : float [0, 1], optional Opacity of the image. + kind : string, one of {'overlay', 'avg'} + The kind of color image desired. 'overlay' cycles over defined colors + and overlays the colored labels over the original image. 'avg' replaces + each labeled segment with its average color, for a stained-class or + pastel painting appearance. Returns ------- @@ -93,6 +98,15 @@ def label2rgb(label, image=None, colors=None, alpha=0.3, The result of blending a cycling colormap (`colors`) for each distinct value in `label` with the image, at a certain alpha value. """ + if kind == 'overlay': + return _label2rgb_overlay(label, image, colors, alpha, bg_label, + bg_color, image_alpha) + else: + return _label2rgb_avg(label, image, bg_label, bg_color) + + +def _label2rgb_overlay(label, image=None, colors=None, alpha=0.3, + bg_label=-1, bg_color=None, image_alpha=1): if colors is None: colors = DEFAULT_COLORS colors = [_rgb_vector(c) for c in colors] @@ -140,3 +154,15 @@ def label2rgb(label, image=None, colors=None, alpha=0.3, result[label == bg_label] = image[label == bg_label] return result + + +def _label2rgb_avg(label_field, image, bg_label, bg_color): + out = np.zeros_like(image) + labels = np.unique(label_field) + if (labels == bg_label).any(): + labels = labels[labels != bg_label] + for label in labels: + mask = (label_field == label).nonzero() + color = image[mask].mean(axis=0) + out[mask] = color + return out From 9b110d6c6e7d6d8261dcd0cd5acec11ea476d73b Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 16 Jun 2014 22:50:49 -0500 Subject: [PATCH 0031/1122] Tweak range definition in `rescale_intensity` --- skimage/_shared/utils.py | 10 ++++ skimage/exposure/exposure.py | 94 ++++++++++++++++++++++++++---------- 2 files changed, 79 insertions(+), 25 deletions(-) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index 612a2b63..6f121955 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -19,6 +19,16 @@ class skimage_deprecation(Warning): pass +def deprecation_warning(msg, **kwargs): + """Emit deprecation warning for scikit-image. + + Unlike default deprecation warnings, this is not silenced by default. + Additional keyword arguments are passed to `warnings.warn_explicit`. + """ + warnings.simplefilter('always', skimage_deprecation) + warnings.warn(msg, category=skimage_deprecation, **kwargs) + + class deprecated(object): """Decorator to mark deprecated functions with warning. diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index 14379f6b..d86caf13 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -3,7 +3,7 @@ import numpy as np from skimage import img_as_float from skimage.util.dtype import dtype_range, dtype_limits -from skimage._shared.utils import deprecated +from skimage._shared.utils import deprecated, deprecation_warning __all__ = ['histogram', 'cumulative_distribution', 'equalize', @@ -136,25 +136,72 @@ def equalize_hist(image, nbins=256): return out.reshape(image.shape) -def rescale_intensity(image, in_range=None, out_range=None): +def _intensity_range(image, range_values, zero_min=None): + """Return image intensity range (min, max) based on desired value type. + + Parameters + ---------- + image : array + Input image. + range_values : {'image' | 'dtype'} or dtype-name or 2-tuple + The image intensity range is configured by this parameter. + + 'image' + Return image min/max as the range. + 'dtype' + Return min/max of the image's dtype as the range. + dtype-name + Valid key in `DTYPE_RANGE`. + 2-tuple + Explicit values for the min/max intensities. + + zero_min : bool + If True, the image dtype's min is truncated to 0. Note that this only + applies the output range if `range_values` specifies a dtype. + """ + if range_values == 'dtype': + range_values = image.dtype.type + + if range_values == 'image': + i_min = np.min(image) + i_max = np.max(image) + elif range_values in DTYPE_RANGE: + i_min, i_max = DTYPE_RANGE[range_values] + if zero_min: + i_min = 0 + else: + i_min, i_max = range_values + return i_min, i_max + + +def rescale_intensity(image, in_range='image', out_range='dtype'): """Return image after stretching or shrinking its intensity levels. The image intensities are uniformly rescaled such that the minimum and - maximum values given by `in_range` match those given by `out_range`. + maximum values given by `in_range` match those given by `out_range`. Thus, + `in_range` values within the input-image's range will clip intensities, + and values outside the input image will tend to push limiting values + (e.g. white and black) toward mean values (e.g. gray). + + The minimum and maximum possible values of the output image is determined + by `out_range`. The actual min/max of the output could lie within this + range if `in_range` lies outside the input-image's intensity range. Parameters ---------- image : array Image array. - in_range : 2-tuple (float, float) or str - Min and max *allowed* intensity values of input image. If None, the - *allowed* min/max values are set to the *actual* min/max values in the - input image. Intensity values outside this range are clipped. - If string, use data limits of dtype specified by the string. - out_range : 2-tuple (float, float) or str - Min and max intensity values of output image. If None, use the min/max - intensities of the image data type. See `skimage.util.dtype` for - details. If string, use data limits of dtype specified by the string. + in_range, out_range : {'image' | 'dtype'} or dtype-name or 2-tuple + Min and max intensity values of input and output image. + + 'image' + Return image min/max as the range. + 'dtype' + Return min/max of the image's dtype as the range. + dtype-name + Valid key in `DTYPE_RANGE`. + 2-tuple + Explicit values for the min/max intensities. Returns ------- @@ -203,20 +250,17 @@ def rescale_intensity(image, in_range=None, out_range=None): dtype = image.dtype.type if in_range is None: - imin = np.min(image) - imax = np.max(image) - elif in_range in DTYPE_RANGE: - imin, imax = DTYPE_RANGE[in_range] - else: - imin, imax = in_range + in_range = 'image' + msg = "`in_range` should not be set to None. Use {!r} instead." + deprecation_warning(msg.format(in_range)) - if out_range is None or out_range in DTYPE_RANGE: - out_range = dtype if out_range is None else out_range - omin, omax = DTYPE_RANGE[out_range] - if imin >= 0: - omin = 0 - else: - omin, omax = out_range + if out_range is None: + out_range = 'dtype' + msg = "`out_range` should not be set to None. Use {!r} instead." + deprecation_warning(msg.format(out_range)) + + imin, imax = _intensity_range(image, in_range) + omin, omax = _intensity_range(image, out_range, zero_min=(imin >= 0)) image = np.clip(image, imin, imax) From 423207e07caea53d021bf847ceba3684e8bc4db3 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 16 Jun 2014 22:54:51 -0500 Subject: [PATCH 0032/1122] Make `intensity_range` a public function --- skimage/exposure/exposure.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index d86caf13..249e4bab 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -136,7 +136,7 @@ def equalize_hist(image, nbins=256): return out.reshape(image.shape) -def _intensity_range(image, range_values, zero_min=None): +def intensity_range(image, range_values='image', zero_min=False): """Return image intensity range (min, max) based on desired value type. Parameters @@ -259,8 +259,8 @@ def rescale_intensity(image, in_range='image', out_range='dtype'): msg = "`out_range` should not be set to None. Use {!r} instead." deprecation_warning(msg.format(out_range)) - imin, imax = _intensity_range(image, in_range) - omin, omax = _intensity_range(image, out_range, zero_min=(imin >= 0)) + imin, imax = intensity_range(image, in_range) + omin, omax = intensity_range(image, out_range, zero_min=(imin >= 0)) image = np.clip(image, imin, imax) From ec9198c58e550ad12ffa78a538c96ca9777024e2 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 16 Jun 2014 23:00:12 -0500 Subject: [PATCH 0033/1122] Edit wording of docstrings --- skimage/_shared/utils.py | 2 +- skimage/exposure/exposure.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index 6f121955..40703206 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -23,7 +23,7 @@ def deprecation_warning(msg, **kwargs): """Emit deprecation warning for scikit-image. Unlike default deprecation warnings, this is not silenced by default. - Additional keyword arguments are passed to `warnings.warn_explicit`. + Additional keyword arguments are passed to `warnings.warn`. """ warnings.simplefilter('always', skimage_deprecation) warnings.warn(msg, category=skimage_deprecation, **kwargs) diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index 249e4bab..1350e61a 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -157,7 +157,7 @@ def intensity_range(image, range_values='image', zero_min=False): zero_min : bool If True, the image dtype's min is truncated to 0. Note that this only - applies the output range if `range_values` specifies a dtype. + applies to the output range if `range_values` specifies a dtype. """ if range_values == 'dtype': range_values = image.dtype.type From 8e539cc12a93658ae9677442cdc554294e0de898 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 16 Jun 2014 23:17:31 -0500 Subject: [PATCH 0034/1122] Clarify docstrings --- skimage/exposure/exposure.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index 1350e61a..2b473fea 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -151,9 +151,13 @@ def intensity_range(image, range_values='image', zero_min=False): 'dtype' Return min/max of the image's dtype as the range. dtype-name - Valid key in `DTYPE_RANGE`. + Return intensity range based on desired `dtype`. Must be valid key + in `DTYPE_RANGE`. Note: `image` is ignored for this range type. 2-tuple - Explicit values for the min/max intensities. + Return `range_values` as min/max intensities. Note that there's no + reason to use this function if you just want to specify the + intensity range explicitly. This option is included for functions + that use `intensity_range` to support all desired range types. zero_min : bool If True, the image dtype's min is truncated to 0. Note that this only @@ -195,13 +199,14 @@ def rescale_intensity(image, in_range='image', out_range='dtype'): Min and max intensity values of input and output image. 'image' - Return image min/max as the range. + Use image min/max as the intensity range. 'dtype' - Return min/max of the image's dtype as the range. + Use min/max of the image's dtype as the intensity range. dtype-name - Valid key in `DTYPE_RANGE`. + Use intensity range based on desired `dtype`. Must be valid key + in `DTYPE_RANGE`. 2-tuple - Explicit values for the min/max intensities. + Use `range_values` as explicit min/max intensities. Returns ------- From b066a143b9c00c1a5c5be6448db3c4b69540442e Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 17 Jun 2014 22:57:20 -0500 Subject: [PATCH 0035/1122] Clarify docstrings --- skimage/exposure/exposure.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index 2b473fea..12161c4e 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -143,8 +143,9 @@ def intensity_range(image, range_values='image', zero_min=False): ---------- image : array Input image. - range_values : {'image' | 'dtype'} or dtype-name or 2-tuple + range_values : str or 2-tuple The image intensity range is configured by this parameter. + The possible values for this parameter are enumerated below. 'image' Return image min/max as the range. @@ -181,22 +182,17 @@ def intensity_range(image, range_values='image', zero_min=False): def rescale_intensity(image, in_range='image', out_range='dtype'): """Return image after stretching or shrinking its intensity levels. - The image intensities are uniformly rescaled such that the minimum and - maximum values given by `in_range` match those given by `out_range`. Thus, - `in_range` values within the input-image's range will clip intensities, - and values outside the input image will tend to push limiting values - (e.g. white and black) toward mean values (e.g. gray). - - The minimum and maximum possible values of the output image is determined - by `out_range`. The actual min/max of the output could lie within this - range if `in_range` lies outside the input-image's intensity range. + The desired intensity range of the input and output, `in_range` and + `out_range` respectively, are used to stretch or shrink the intensity range + of the input image. See examples below. Parameters ---------- image : array Image array. - in_range, out_range : {'image' | 'dtype'} or dtype-name or 2-tuple + in_range, out_range : str or 2-tuple Min and max intensity values of input and output image. + The possible values for this parameter are enumerated below. 'image' Use image min/max as the intensity range. @@ -216,7 +212,9 @@ def rescale_intensity(image, in_range='image', out_range='dtype'): Examples -------- - By default, intensities are stretched to the limits allowed by the dtype: + By default, the min/max intensities of the input image are stretched to + the limits allowed by the image's dtype, since `in_range` defaults to + 'image' and `out_range` defaults to 'dtype': >>> image = np.array([51, 102, 153], dtype=np.uint8) >>> rescale_intensity(image) From 4c0befed6290b34b32991e63fb090106e0824062 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 17 Jun 2014 22:59:04 -0500 Subject: [PATCH 0036/1122] Add TODO for deprecation removal --- TODO.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/TODO.txt b/TODO.txt index adc70f48..e2fc4218 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,5 +1,9 @@ Remember to list any API changes below in `doc/source/api_changes.txt`. +Version 0.13 +------------ +* Remove deprecated `None` defaults for `skimage.exposure.rescale_intensity` + Version 0.12 ------------ * Change `label` to mark background as 0, not -1, which is consistent with @@ -7,7 +11,7 @@ Version 0.12 * Remove `skimage.morphology.label` from `skimage.morphology.__init__`--it now lives in `skimage.measure.label`. * Remove deprecated `reverse_map` parameter of `skimage.transform.warp` -* Change depecrated `enforce_connectivity=False` on skimage.segmentation.slic +* Change deprecated `enforce_connectivity=False` on skimage.segmentation.slic and set it to True as default * Remove deprecated `skimage.measure.fit.BaseModel._params` attribute * Remove deprecated `skimage.measure.fit.BaseModel._params`, From 7cc40ddbff479cc477f37bb37cd368b577af88aa Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 18 Jun 2014 16:44:02 +1000 Subject: [PATCH 0037/1122] Add background label handling for label2rgb_avg --- skimage/color/colorlabel.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/skimage/color/colorlabel.py b/skimage/color/colorlabel.py index 47cb0de9..8074c2a3 100644 --- a/skimage/color/colorlabel.py +++ b/skimage/color/colorlabel.py @@ -159,8 +159,10 @@ def _label2rgb_overlay(label, image=None, colors=None, alpha=0.3, def _label2rgb_avg(label_field, image, bg_label, bg_color): out = np.zeros_like(image) labels = np.unique(label_field) - if (labels == bg_label).any(): + bg = (labels == bg_label) + if bg.any(): labels = labels[labels != bg_label] + out[bg] = bg_color for label in labels: mask = (label_field == label).nonzero() color = image[mask].mean(axis=0) From d72163a0379dff0d1d7d39d2536a969f9189d4f0 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 18 Jun 2014 16:46:15 +1000 Subject: [PATCH 0038/1122] Add docstring for _label2rgb_avg --- skimage/color/colorlabel.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/skimage/color/colorlabel.py b/skimage/color/colorlabel.py index 8074c2a3..f76eeebb 100644 --- a/skimage/color/colorlabel.py +++ b/skimage/color/colorlabel.py @@ -157,6 +157,24 @@ def _label2rgb_overlay(label, image=None, colors=None, alpha=0.3, def _label2rgb_avg(label_field, image, bg_label, bg_color): + """Visualise each segment in `label_field` with its mean color in `image`. + + Parameters + ---------- + label_field : array of int + A segmentation of an image. + image : array, shape ``label_field.shape + (3,)`` + A color image of the same spatial shape as `label_field`. + bg_label : int, optional + A value in `label_field` to be treated as background. + bg_color : 3-tuple of int, optional + The color for the background label + + Returns + ------- + out : array, same shape and type as `image` + The output visualization. + """ out = np.zeros_like(image) labels = np.unique(label_field) bg = (labels == bg_label) From b70d423aab5a443cf608c65c880d163c73b2190e Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 18 Jun 2014 16:46:51 +1000 Subject: [PATCH 0039/1122] Add default values for background in _label2rgb_avg --- skimage/color/colorlabel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/color/colorlabel.py b/skimage/color/colorlabel.py index f76eeebb..e373dc16 100644 --- a/skimage/color/colorlabel.py +++ b/skimage/color/colorlabel.py @@ -156,7 +156,7 @@ def _label2rgb_overlay(label, image=None, colors=None, alpha=0.3, return result -def _label2rgb_avg(label_field, image, bg_label, bg_color): +def _label2rgb_avg(label_field, image, bg_label=0, bg_color=(0, 0, 0)): """Visualise each segment in `label_field` with its mean color in `image`. Parameters From b494e3622364518e5a4f40ebe7c22498c18fd23a Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 18 Jun 2014 17:03:21 +1000 Subject: [PATCH 0040/1122] Add docstring for _label2rgb_overlay --- skimage/color/colorlabel.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/skimage/color/colorlabel.py b/skimage/color/colorlabel.py index e373dc16..c030f849 100644 --- a/skimage/color/colorlabel.py +++ b/skimage/color/colorlabel.py @@ -107,6 +107,34 @@ def label2rgb(label, image=None, colors=None, alpha=0.3, def _label2rgb_overlay(label, image=None, colors=None, alpha=0.3, bg_label=-1, bg_color=None, image_alpha=1): + """Return an RGB image where color-coded labels are painted over the image. + + Parameters + ---------- + label : array, shape (M, N) + Integer array of labels with the same shape as `image`. + image : array, shape (M, N, 3), optional + Image used as underlay for labels. If the input is an RGB image, it's + converted to grayscale before coloring. + colors : list, optional + List of colors. If the number of labels exceeds the number of colors, + then the colors are cycled. + alpha : float [0, 1], optional + Opacity of colorized labels. Ignored if image is `None`. + bg_label : int, optional + Label that's treated as the background. + bg_color : str or array, optional + Background color. Must be a name in `color_dict` or RGB float values + between [0, 1]. + image_alpha : float [0, 1], optional + Opacity of the image. + + Returns + ------- + result : array of float, shape (M, N, 3) + The result of blending a cycling colormap (`colors`) for each distinct + value in `label` with the image, at a certain alpha value. + """ if colors is None: colors = DEFAULT_COLORS colors = [_rgb_vector(c) for c in colors] From fdc848b6a7fa1c16ac424850927e03d537568bc5 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 18 Jun 2014 17:03:34 +1000 Subject: [PATCH 0041/1122] Add tests for label2rgb avg mode --- skimage/color/tests/test_colorlabel.py | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/skimage/color/tests/test_colorlabel.py b/skimage/color/tests/test_colorlabel.py index dcfbe4ea..357a3a74 100644 --- a/skimage/color/tests/test_colorlabel.py +++ b/skimage/color/tests/test_colorlabel.py @@ -89,6 +89,39 @@ def test_leave_labels_alone(): label2rgb(labels, bg_label=1) assert_array_equal(labels, labels_saved) +def test_avg(): + label_field = np.array([[1, 1, 1, 2], + [1, 2, 2, 2], + [3, 3, 3, 3]], dtype=np.uint8) + r = np.array([[1., 1., 0., 0.], + [0., 0., 1., 1.], + [0., 0., 0., 0.]]) + g = np.array([[0., 0., 0., 1.], + [1., 1., 1., 0.], + [0., 0., 0., 0.]]) + b = np.array([[0., 0., 0., 1.], + [0., 1., 1., 1.], + [0., 0., 1., 1.]]) + image = np.dstack((r, g, b)) + out = label2rgb(label_field, image, kind='avg') + rout = np.array([[0.5, 0.5, 0.5, 0.5], + [0.5, 0.5, 0.5, 0.5], + [0. , 0. , 0. , 0. ]]) + gout = np.array([[0.25, 0.25, 0.25, 0.75], + [0.25, 0.75, 0.75, 0.75], + [0. , 0. , 0. , 0. ]]) + bout = np.array([[0. , 0. , 0. , 1. ], + [0. , 1. , 1. , 1. ], + [0.5, 0.5, 0.5, 0.5]]) + expected_out = np.dstack((rout, gout, bout)) + assert_array_equal(out, expected_out) + + out_bg = label2rgb(label_field, image, bg_label=2, bg_color=(0, 0, 0), + kind='avg') + expected_out_bg = expected_out.copy() + expected_out_bg[label_field == 2] = 0 + assert_array_equal(out_bg, expected_out_bg) + if __name__ == '__main__': testing.run_module_suite() From 21c80d25673564e4a16459789701da0ab5c04dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 18 Jun 2014 10:57:34 -0400 Subject: [PATCH 0042/1122] Test negative intensities --- skimage/color/tests/test_colorlabel.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/skimage/color/tests/test_colorlabel.py b/skimage/color/tests/test_colorlabel.py index 357a3a74..4daf3a66 100644 --- a/skimage/color/tests/test_colorlabel.py +++ b/skimage/color/tests/test_colorlabel.py @@ -3,8 +3,9 @@ import itertools import numpy as np from numpy import testing from skimage.color.colorlabel import label2rgb +from skimage._shared.utils import all_warnings from numpy.testing import (assert_array_almost_equal as assert_close, - assert_array_equal) + assert_array_equal, assert_warns) def test_shape_mismatch(): @@ -123,6 +124,13 @@ def test_avg(): assert_array_equal(out_bg, expected_out_bg) +def test_negative_intensity(): + with all_warnings(): + labels = np.arange(100).reshape(10, 10) + image = -1 * np.ones((10, 10)) + assert_warns(UserWarning, label2rgb, labels, image) + + if __name__ == '__main__': testing.run_module_suite() From 2b10d9817944de7cac622a00f73a96876913584d Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 16 Jun 2014 02:04:42 +0530 Subject: [PATCH 0043/1122] Added mean color RAG construction code --- skimage/graph/_construct.pyx | 126 +++++++++++++++++++++++++++++++++++ skimage/graph/graph.py | 23 +++++++ skimage/graph/setup.py | 5 ++ 3 files changed, 154 insertions(+) create mode 100644 skimage/graph/_construct.pyx create mode 100644 skimage/graph/graph.py diff --git a/skimage/graph/_construct.pyx b/skimage/graph/_construct.pyx new file mode 100644 index 00000000..3b91fe64 --- /dev/null +++ b/skimage/graph/_construct.pyx @@ -0,0 +1,126 @@ +import networkx as nx +cimport numpy as cnp +import numpy as np + + +def construct_rag_meancolor_3d( img, arr): + cdef Py_ssize_t l, b, h, i, j, k + cdef cnp.int32_t current, next + l = arr.shape[0] + b = arr.shape[1] + h = arr.shape[2] + + g = nx.Graph() + + i = 0 + while i < l - 1: + j = 0 + while j < b - 1: + k = 0 + while k < h - 1: + current = arr[i, j, k] + + try : + g.node[current]['pixel_count'] += 1 + g.node[current]['total_color'] += img[i,j] + except KeyError: + g.add_node(current) + g.node[current]['pixel_count'] = 1 + g.node[current]['total_color'] = img[i,j].astype(np.long) + g.node[current]['labels'] = [arr[i,j]] + + next = arr[i + 1, j, k] + if current != next: + g.add_edge(current, next) + + next = arr[i, j + 1, k] + if current != next: + g.add_edge(current, next) + + next = arr[i + 1, j + 1, k] + if current != next: + g.add_edge(current, next) + + next = arr[i + 1, j, k + 1] + if current != next: + g.add_edge(current, next) + + next = arr[i, j + 1, k + 1] + if current != next: + g.add_edge(current, next) + + next = arr[i + 1, j + 1, k + 1] + if current != next: + g.add_edge(current, next) + + next = arr[i, j, k + 1] + if current != next: + g.add_edge(current, next) + + + k += 1 + + j += 1 + + i += 1 + + + for n in g.nodes(): + g.node[n]['mean_color'] = g.node[n]['total_color']/g.node[n]['pixel_count'] + + for x,y in g.edges_iter() : + diff = g.node[x]['mean_color'] - g.node[y]['mean_color'] + g[x][y]['weight'] = np.sqrt(diff.dot(diff)) + + return g + + +def construct_rag_meancolor_2d(img, arr): + cdef Py_ssize_t l, b, h, i, j, k + cdef cnp.int32_t current, next + l = arr.shape[0] + b = arr.shape[1] + + g = nx.Graph() + + i = 0 + while i < l - 1: + j = 0 + while j < b - 1: + current = arr[i, j] + + try : + g.node[current]['pixel_count'] += 1 + g.node[current]['total_color'] += img[i,j] + except KeyError: + g.add_node(current) + g.node[current]['pixel_count'] = 1 + g.node[current]['total_color'] = img[i,j].astype(np.long) + g.node[current]['labels'] = [arr[i,j]] + + next = arr[i + 1, j] + if current != next: + g.add_edge(current, next) + + next = arr[i, j + 1] + if current != next: + g.add_edge(current, next) + + next = arr[i + 1, j + 1] + if current != next: + g.add_edge(current, next) + + j += 1 + + i += 1 + + + for n in g.nodes(): + g.node[n]['mean_color'] = g.node[n]['total_color']/g.node[n]['pixel_count'] + + for x,y in g.edges_iter() : + diff = g.node[x]['mean_color'] - g.node[y]['mean_color'] + g[x][y]['weight'] = np.sqrt(diff.dot(diff)) + + + return g diff --git a/skimage/graph/graph.py b/skimage/graph/graph.py new file mode 100644 index 00000000..2597e985 --- /dev/null +++ b/skimage/graph/graph.py @@ -0,0 +1,23 @@ +import netwrokx as nx + +class Graph(nx.Graph): + + def merge_nodes(i,j): + if not self.has_edge(i, j): + raise ValueError('Cant merge non adjacent nodes') + + # print "before ",self.order() + for x in self.neighbors(i): + if x == j: + continue + w1 = self.get_edge_data(x, i)['weight'] + w2 = -1 + if self.has_edge(x, j): + w2 = self.get_edge_data(x, j)['weight'] + + w = max(w1, w2) + + self.add_edge(x, j, weight=w) + + self.node[j]['labels'] += self.node[i]['labels'] + self.remove_node(i) diff --git a/skimage/graph/setup.py b/skimage/graph/setup.py index 463d2739..252eeed2 100644 --- a/skimage/graph/setup.py +++ b/skimage/graph/setup.py @@ -17,6 +17,8 @@ def configuration(parent_package='', top_path=None): cython(['_spath.pyx'], working_path=base_path) cython(['_mcp.pyx'], working_path=base_path) cython(['heap.pyx'], working_path=base_path) + cython(['_construct.pyx'], working_path=base_path) + config.add_extension('_spath', sources=['_spath.c'], include_dirs=[get_numpy_include_dirs()]) @@ -24,6 +26,9 @@ def configuration(parent_package='', top_path=None): include_dirs=[get_numpy_include_dirs()]) config.add_extension('heap', sources=['heap.c'], include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_construct', sources=['_construct.c'], + include_dirs=[get_numpy_include_dirs()]) + return config From 53077c9f227dac53a93edd50176ea2e6ce44ab3d Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 16 Jun 2014 03:30:01 +0530 Subject: [PATCH 0044/1122] Added edge threshold cut --- skimage/graph/graph_cut.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 skimage/graph/graph_cut.py diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py new file mode 100644 index 00000000..bf55db84 --- /dev/null +++ b/skimage/graph/graph_cut.py @@ -0,0 +1,25 @@ +import networkx as nx +import numpy as np + +def threshold_cut(label, rag, thresh): + + #print [rag.edges_iter(data = True)] + to_remove = [(x,y) for x,y,d in rag.edges_iter(data = True) if d['weight'] >= thresh] + print "edges to remove",len(to_remove) + + rag.remove_edges_from(to_remove) + + + comps = nx.connected_components(rag) + out = np.copy(label) + print "comps",len(comps) + + for i, nodes in enumerate(comps) : + + for node in nodes : + for l in rag.node[node]['labels'] : + out[label == l] = i + + #print out + #print label + return out From a6c9a5a2a7e1b6f858134a1079f8e38d3a968f11 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 16 Jun 2014 17:55:40 +0530 Subject: [PATCH 0045/1122] Renamed graph.py to rag.py --- skimage/graph/_construct.pyx | 6 +++--- skimage/graph/rag.py | 35 +++++++++++++++++++++++++++++++++ skimage/graph/tests/test_rag.py | 10 ++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 skimage/graph/rag.py create mode 100644 skimage/graph/tests/test_rag.py diff --git a/skimage/graph/_construct.pyx b/skimage/graph/_construct.pyx index 3b91fe64..ece98143 100644 --- a/skimage/graph/_construct.pyx +++ b/skimage/graph/_construct.pyx @@ -1,4 +1,4 @@ -import networkx as nx +import rag cimport numpy as cnp import numpy as np @@ -10,7 +10,7 @@ def construct_rag_meancolor_3d( img, arr): b = arr.shape[1] h = arr.shape[2] - g = nx.Graph() + g = rag.RAG() i = 0 while i < l - 1: @@ -81,7 +81,7 @@ def construct_rag_meancolor_2d(img, arr): l = arr.shape[0] b = arr.shape[1] - g = nx.Graph() + g = rag.RAG() i = 0 while i < l - 1: diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py new file mode 100644 index 00000000..9795787b --- /dev/null +++ b/skimage/graph/rag.py @@ -0,0 +1,35 @@ +import networkx as nx +import _construct +from skimage import util + +class RAG(nx.Graph): + + def merge_nodes(i,j): + if not self.has_edge(i, j): + raise ValueError('Cant merge non adjacent nodes') + + # print "before ",self.order() + for x in self.neighbors(i): + if x == j: + continue + w1 = self.get_edge_data(x, i)['weight'] + w2 = -1 + if self.has_edge(x, j): + w2 = self.get_edge_data(x, j)['weight'] + + w = max(w1, w2) + + self.add_edge(x, j, weight=w) + + self.node[j]['labels'] += self.node[i]['labels'] + self.remove_node(i) + +def rag_meancolor(img,labels): + + img = util.img_as_ubyte(img) + if img.ndim == 3 : + return _construct.construct_rag_meancolor_3d(img,labels) + elif img.ndim == 2 : + return _construct.construct_rag_meancolor_2d(img,labels) + else : + raise ValueError("Image dimension not supported") diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py new file mode 100644 index 00000000..08d1a1cf --- /dev/null +++ b/skimage/graph/tests/test_rag.py @@ -0,0 +1,10 @@ +import numpy as np + +def test_threshold_cut(): + arr = np.array((100,100,3),dtype='uint8') + arr[:50,:50] = 0 + arr[:50,50:] = 1 + arr[50:,50:] = 2 + arr[50:,50:] = 3 + + From abcb9cf2efca91c320458fec7a67ba8782b30df0 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 16 Jun 2014 19:22:47 +0530 Subject: [PATCH 0046/1122] Added testcase --- skimage/graph/__init__.py | 6 +++++- skimage/graph/graph.py | 23 ----------------------- skimage/graph/graph_cut.py | 5 +++-- skimage/graph/rag.py | 4 ++-- skimage/graph/tests/test_rag.py | 29 ++++++++++++++++++++++++----- 5 files changed, 34 insertions(+), 33 deletions(-) delete mode 100644 skimage/graph/graph.py diff --git a/skimage/graph/__init__.py b/skimage/graph/__init__.py index a335971d..90da2088 100644 --- a/skimage/graph/__init__.py +++ b/skimage/graph/__init__.py @@ -1,9 +1,13 @@ from .spath import shortest_path from .mcp import MCP, MCP_Geometric, MCP_Connect, MCP_Flexible, route_through_array +from .rag import rag_meancolor +from .graph_cut import threshold_cut __all__ = ['shortest_path', 'MCP', 'MCP_Geometric', 'MCP_Connect', 'MCP_Flexible', - 'route_through_array'] \ No newline at end of file + 'route_through_array', + 'rag_meancolor', + 'threshold_cut'] diff --git a/skimage/graph/graph.py b/skimage/graph/graph.py deleted file mode 100644 index 2597e985..00000000 --- a/skimage/graph/graph.py +++ /dev/null @@ -1,23 +0,0 @@ -import netwrokx as nx - -class Graph(nx.Graph): - - def merge_nodes(i,j): - if not self.has_edge(i, j): - raise ValueError('Cant merge non adjacent nodes') - - # print "before ",self.order() - for x in self.neighbors(i): - if x == j: - continue - w1 = self.get_edge_data(x, i)['weight'] - w2 = -1 - if self.has_edge(x, j): - w2 = self.get_edge_data(x, j)['weight'] - - w = max(w1, w2) - - self.add_edge(x, j, weight=w) - - self.node[j]['labels'] += self.node[i]['labels'] - self.remove_node(i) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index bf55db84..5d6a98e2 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -5,14 +5,15 @@ def threshold_cut(label, rag, thresh): #print [rag.edges_iter(data = True)] to_remove = [(x,y) for x,y,d in rag.edges_iter(data = True) if d['weight'] >= thresh] - print "edges to remove",len(to_remove) + #print "edges to remove",len(to_remove) rag.remove_edges_from(to_remove) + #print "to remove", to_remove comps = nx.connected_components(rag) out = np.copy(label) - print "comps",len(comps) + #print "comps",len(comps) for i, nodes in enumerate(comps) : diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 9795787b..dd721959 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -27,9 +27,9 @@ class RAG(nx.Graph): def rag_meancolor(img,labels): img = util.img_as_ubyte(img) - if img.ndim == 3 : + if img.ndim == 4 : return _construct.construct_rag_meancolor_3d(img,labels) - elif img.ndim == 2 : + elif img.ndim == 3 : return _construct.construct_rag_meancolor_2d(img,labels) else : raise ValueError("Image dimension not supported") diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 08d1a1cf..d24c62c4 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -1,10 +1,29 @@ import numpy as np +from skimage import graph def test_threshold_cut(): - arr = np.array((100,100,3),dtype='uint8') - arr[:50,:50] = 0 - arr[:50,50:] = 1 - arr[50:,50:] = 2 - arr[50:,50:] = 3 + + img = np.zeros((100,100,3),dtype='uint8') + img[:50,:50] = 255,255,255 + img[:50,50:] = 254,254,254 + img[50:,:50] = 2,2,2 + img[50:,50:] = 1,1,1 + + + + labels = np.zeros((100,100),dtype='uint8') + labels[:50,:50] = 0 + labels[:50,50:] = 1 + labels[50:,:50] = 2 + labels[50:,50:] = 3 + + + #print labels + rag = graph.rag_meancolor(img, labels) + #print "no of edges",rag.number_of_edges() + new_labels = graph.threshold_cut(labels, rag, 10) + + assert new_labels.max() == 2 + #assert False From 10f91fe8b0174c03d56252f2a14c6018276db8d1 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 16 Jun 2014 21:59:36 +0530 Subject: [PATCH 0047/1122] Added docs --- skimage/graph/_construct.pyx | 76 +++++++++++++++++++++++++++--------- skimage/graph/graph_cut.py | 27 ++++++++++--- skimage/graph/rag.py | 47 +++++++++++++++++++++- 3 files changed, 124 insertions(+), 26 deletions(-) diff --git a/skimage/graph/_construct.pyx b/skimage/graph/_construct.pyx index ece98143..c4fbb465 100644 --- a/skimage/graph/_construct.pyx +++ b/skimage/graph/_construct.pyx @@ -3,7 +3,28 @@ cimport numpy as cnp import numpy as np -def construct_rag_meancolor_3d( img, arr): +def construct_rag_meancolor_3d(img, arr): + """Computes the Region Adjacency Graph of a 3D color image using + difference in mean color of regions as edge weights. + + Given an image and its segmentation, this method constructs the + corresponsing Region Adjacency Graph (RAG).Each node in the RAG + represents a contiguous pixels with in `img` the same label in + `arr` + + Parameters + ---------- + img : (width, height, depth, 3) ndarray + Input image. + arr : (width, height, depth) ndarray + The array with labels. + + Returns + ------- + out : RAG + The region adjacency graph. + """ + cdef Py_ssize_t l, b, h, i, j, k cdef cnp.int32_t current, next l = arr.shape[0] @@ -19,15 +40,15 @@ def construct_rag_meancolor_3d( img, arr): k = 0 while k < h - 1: current = arr[i, j, k] - - try : + + try: g.node[current]['pixel_count'] += 1 - g.node[current]['total_color'] += img[i,j] + g.node[current]['total_color'] += img[i, j] except KeyError: g.add_node(current) g.node[current]['pixel_count'] = 1 - g.node[current]['total_color'] = img[i,j].astype(np.long) - g.node[current]['labels'] = [arr[i,j]] + g.node[current]['total_color'] = img[i, j].astype(np.long) + g.node[current]['labels'] = [arr[i, j]] next = arr[i + 1, j, k] if current != next: @@ -57,18 +78,17 @@ def construct_rag_meancolor_3d( img, arr): if current != next: g.add_edge(current, next) - k += 1 j += 1 i += 1 - for n in g.nodes(): - g.node[n]['mean_color'] = g.node[n]['total_color']/g.node[n]['pixel_count'] + g.node[n]['mean_color'] = g.node[n][ + 'total_color'] / g.node[n]['pixel_count'] - for x,y in g.edges_iter() : + for x, y in g.edges_iter(): diff = g.node[x]['mean_color'] - g.node[y]['mean_color'] g[x][y]['weight'] = np.sqrt(diff.dot(diff)) @@ -76,6 +96,27 @@ def construct_rag_meancolor_3d( img, arr): def construct_rag_meancolor_2d(img, arr): + """Computes the Region Adjacency Graph of a 2D color image using + difference in mean color of regions as edge weights. + + Given an image and its segmentation, this method constructs the + corresponsing Region Adjacency Graph (RAG).Each node in the RAG + represents a contiguous pixels with in `img` the same label in + `arr` + + Parameters + ---------- + img : (width, height, 3) ndarray + Input image. + arr : (width, height) ndarray + The array with labels. + + Returns + ------- + out : RAG + The region adjacency graph. + """ + cdef Py_ssize_t l, b, h, i, j, k cdef cnp.int32_t current, next l = arr.shape[0] @@ -89,14 +130,14 @@ def construct_rag_meancolor_2d(img, arr): while j < b - 1: current = arr[i, j] - try : + try: g.node[current]['pixel_count'] += 1 - g.node[current]['total_color'] += img[i,j] + g.node[current]['total_color'] += img[i, j] except KeyError: g.add_node(current) g.node[current]['pixel_count'] = 1 - g.node[current]['total_color'] = img[i,j].astype(np.long) - g.node[current]['labels'] = [arr[i,j]] + g.node[current]['total_color'] = img[i, j].astype(np.long) + g.node[current]['labels'] = [arr[i, j]] next = arr[i + 1, j] if current != next: @@ -114,13 +155,12 @@ def construct_rag_meancolor_2d(img, arr): i += 1 - for n in g.nodes(): - g.node[n]['mean_color'] = g.node[n]['total_color']/g.node[n]['pixel_count'] + g.node[n]['mean_color'] = g.node[n][ + 'total_color'] / g.node[n]['pixel_count'] - for x,y in g.edges_iter() : + for x, y in g.edges_iter(): diff = g.node[x]['mean_color'] - g.node[y]['mean_color'] g[x][y]['weight'] = np.sqrt(diff.dot(diff)) - return g diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 5d6a98e2..939681af 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -2,18 +2,34 @@ import networkx as nx import numpy as np def threshold_cut(label, rag, thresh): + """Combines regions seperated by weight less than threshold. - #print [rag.edges_iter(data = True)] + Given an image's labels and its RAG, outputs new labels by + combining regions whose nodes are seperated by a weight less + than the given threshold. + + Parameters + ---------- + label : (width, height, 3) or (width, height, depth, 3) ndarray + The array of labels. + rag : RAG + The region adjacency graph. + thresh : float + The threshold, regions with edge weights less than this + are combined. + + Returns + ------- + out : (width, height, 3) or (width, height, depth, 3) ndarray + The new labelled array. + """ to_remove = [(x,y) for x,y,d in rag.edges_iter(data = True) if d['weight'] >= thresh] - #print "edges to remove",len(to_remove) rag.remove_edges_from(to_remove) - #print "to remove", to_remove comps = nx.connected_components(rag) out = np.copy(label) - #print "comps",len(comps) for i, nodes in enumerate(comps) : @@ -21,6 +37,5 @@ def threshold_cut(label, rag, thresh): for l in rag.node[node]['labels'] : out[label == l] = i - #print out - #print label + return out diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index dd721959..44ec76f9 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -3,8 +3,30 @@ import _construct from skimage import util class RAG(nx.Graph): + """ + The class for holding the Region Adjacency Graph (RAG). + + Each region is a contiguous set of pixels in an image, usuall + sharing some common property.Adjacent regions have an edge + between their corresponding nodes. + """ + + def merge_nodes(self,i,j): + """Merges nodes `i` and `j`. + + The new combined node is adjacent to all the neighbors of `i` + and `j`. In case of conflicting edges, edge with higher weight + is chosen. + + Parameters + ---------- + i : int + Node to be merged. + j : int + Node to be merged. + + """ - def merge_nodes(i,j): if not self.has_edge(i, j): raise ValueError('Cant merge non adjacent nodes') @@ -24,7 +46,28 @@ class RAG(nx.Graph): self.node[j]['labels'] += self.node[i]['labels'] self.remove_node(i) -def rag_meancolor(img,labels): + +def rag_meancolor(img, labels): + """Computes the Region Adjacency Graph of a color image using + difference in mean color of regions as edge weights. + + Given an image and its segmentation, this method constructs the + corresponsing Region Adjacency Graph (RAG).Each node in the RAG + represents a contiguous pixels with in `img` the same label in + `arr` + + Parameters + ---------- + img : (width, height, 3) or (width, height, depth, 3) ndarray + Input image. + arr : (width, height) or (width, height, depth) ndarray + The array with labels. + + Returns + ------- + out : RAG + The region adjacency graph. + """ img = util.img_as_ubyte(img) if img.ndim == 4 : From 850f835114881b7ce179f322bb588c950461ae63 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 16 Jun 2014 22:17:13 +0530 Subject: [PATCH 0048/1122] Docs formatting --- skimage/graph/graph_cut.py | 18 +++++++++--------- skimage/graph/rag.py | 18 ++++++++++-------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 939681af..0f28b453 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -1,11 +1,12 @@ import networkx as nx import numpy as np + def threshold_cut(label, rag, thresh): """Combines regions seperated by weight less than threshold. Given an image's labels and its RAG, outputs new labels by - combining regions whose nodes are seperated by a weight less + combining regions whose nodes are seperated by a weight less than the given threshold. Parameters @@ -14,7 +15,7 @@ def threshold_cut(label, rag, thresh): The array of labels. rag : RAG The region adjacency graph. - thresh : float + thresh : float The threshold, regions with edge weights less than this are combined. @@ -23,19 +24,18 @@ def threshold_cut(label, rag, thresh): out : (width, height, 3) or (width, height, depth, 3) ndarray The new labelled array. """ - to_remove = [(x,y) for x,y,d in rag.edges_iter(data = True) if d['weight'] >= thresh] + to_remove = [(x, y) + for x, y, d in rag.edges_iter(data=True) if d['weight'] >= thresh] rag.remove_edges_from(to_remove) - comps = nx.connected_components(rag) out = np.copy(label) - for i, nodes in enumerate(comps) : - - for node in nodes : - for l in rag.node[node]['labels'] : + for i, nodes in enumerate(comps): + + for node in nodes: + for l in rag.node[node]['labels']: out[label == l] = i - return out diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 44ec76f9..2cf353de 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -2,16 +2,18 @@ import networkx as nx import _construct from skimage import util + class RAG(nx.Graph): + """ The class for holding the Region Adjacency Graph (RAG). Each region is a contiguous set of pixels in an image, usuall - sharing some common property.Adjacent regions have an edge + sharing some common property.Adjacent regions have an edge between their corresponding nodes. """ - def merge_nodes(self,i,j): + def merge_nodes(self, i, j): """Merges nodes `i` and `j`. The new combined node is adjacent to all the neighbors of `i` @@ -43,7 +45,7 @@ class RAG(nx.Graph): self.add_edge(x, j, weight=w) - self.node[j]['labels'] += self.node[i]['labels'] + self.node[j]['labels'] += self.node[i]['labels'] self.remove_node(i) @@ -70,9 +72,9 @@ def rag_meancolor(img, labels): """ img = util.img_as_ubyte(img) - if img.ndim == 4 : - return _construct.construct_rag_meancolor_3d(img,labels) - elif img.ndim == 3 : - return _construct.construct_rag_meancolor_2d(img,labels) - else : + if img.ndim == 4: + return _construct.construct_rag_meancolor_3d(img, labels) + elif img.ndim == 3: + return _construct.construct_rag_meancolor_2d(img, labels) + else: raise ValueError("Image dimension not supported") From 1ad8ad733b34cd67462e6bf526d87d97ec14d1fa Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 16 Jun 2014 22:59:23 +0530 Subject: [PATCH 0049/1122] Added examples in docstring --- skimage/graph/graph_cut.py | 9 +++++++++ skimage/graph/rag.py | 30 +++++++++++++++++++----------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 0f28b453..2733bf0e 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -23,6 +23,15 @@ def threshold_cut(label, rag, thresh): ------- out : (width, height, 3) or (width, height, depth, 3) ndarray The new labelled array. + + Examples + -------- + >>> from skimage import data,graph,segmentation + >>> img = data.lena() + >>> labels = segmentation.slic(img) + >>> rag = graph.rag_meancolor(img, labels) + >>> new_labels = graph.threshold_cut(labels, rag, 10) + """ to_remove = [(x, y) for x, y, d in rag.edges_iter(data=True) if d['weight'] >= thresh] diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 2cf353de..af24af18 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -14,20 +14,20 @@ class RAG(nx.Graph): """ def merge_nodes(self, i, j): - """Merges nodes `i` and `j`. + """Merges nodes `i` and `j`. - The new combined node is adjacent to all the neighbors of `i` - and `j`. In case of conflicting edges, edge with higher weight - is chosen. + The new combined node is adjacent to all the neighbors of `i` + and `j`. In case of conflicting edges, edge with higher weight + is chosen. - Parameters - ---------- - i : int - Node to be merged. - j : int - Node to be merged. + Parameters + ---------- + i : int + Node to be merged. + j : int + Node to be merged. - """ + """ if not self.has_edge(i, j): raise ValueError('Cant merge non adjacent nodes') @@ -69,6 +69,14 @@ def rag_meancolor(img, labels): ------- out : RAG The region adjacency graph. + + Examples + -------- + >>> from skimage import data,graph,segmentation + >>> img = data.lena() + >>> labels = segmentation.slic(img) + >>> rag = graph.rag_meancolor(img, labels) + """ img = util.img_as_ubyte(img) From 3ea78096e7ef5bae3e97a89a132acb73e0a39f9c Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 16 Jun 2014 23:02:01 +0530 Subject: [PATCH 0050/1122] Formatting changes --- skimage/graph/graph_cut.py | 5 ++--- skimage/graph/tests/test_rag.py | 34 +++++++++++++++------------------ 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 2733bf0e..7c183689 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -31,10 +31,9 @@ def threshold_cut(label, rag, thresh): >>> labels = segmentation.slic(img) >>> rag = graph.rag_meancolor(img, labels) >>> new_labels = graph.threshold_cut(labels, rag, 10) - """ - to_remove = [(x, y) - for x, y, d in rag.edges_iter(data=True) if d['weight'] >= thresh] + to_remove = [(x, y) for x, y, d in rag.edges_iter(data=True) + if d['weight'] >= thresh] rag.remove_edges_from(to_remove) diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index d24c62c4..fdf3fee7 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -1,29 +1,25 @@ import numpy as np from skimage import graph + def test_threshold_cut(): - - img = np.zeros((100,100,3),dtype='uint8') - img[:50,:50] = 255,255,255 - img[:50,50:] = 254,254,254 - img[50:,:50] = 2,2,2 - img[50:,50:] = 1,1,1 + img = np.zeros((100, 100, 3), dtype='uint8') + img[:50, :50] = 255, 255, 255 + img[:50, 50:] = 254, 254, 254 + img[50:, :50] = 2, 2, 2 + img[50:, 50:] = 1, 1, 1 + labels = np.zeros((100, 100), dtype='uint8') + labels[:50, :50] = 0 + labels[:50, 50:] = 1 + labels[50:, :50] = 2 + labels[50:, 50:] = 3 - labels = np.zeros((100,100),dtype='uint8') - labels[:50,:50] = 0 - labels[:50,50:] = 1 - labels[50:,:50] = 2 - labels[50:,50:] = 3 - - - #print labels + # print labels rag = graph.rag_meancolor(img, labels) - #print "no of edges",rag.number_of_edges() + # print "no of edges",rag.number_of_edges() new_labels = graph.threshold_cut(labels, rag, 10) - - assert new_labels.max() == 2 - #assert False - + assert new_labels.max() == 2 + # assert False From c08d96d2b5738395cc70f5152d88ed8d3b7c4b10 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 16 Jun 2014 23:03:36 +0530 Subject: [PATCH 0051/1122] Minor Foramtting --- skimage/graph/graph_cut.py | 1 - skimage/graph/tests/test_rag.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 7c183689..d6e1d013 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -34,7 +34,6 @@ def threshold_cut(label, rag, thresh): """ to_remove = [(x, y) for x, y, d in rag.edges_iter(data=True) if d['weight'] >= thresh] - rag.remove_edges_from(to_remove) comps = nx.connected_components(rag) diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index fdf3fee7..8c28febe 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -16,10 +16,7 @@ def test_threshold_cut(): labels[50:, :50] = 2 labels[50:, 50:] = 3 - # print labels rag = graph.rag_meancolor(img, labels) - # print "no of edges",rag.number_of_edges() new_labels = graph.threshold_cut(labels, rag, 10) assert new_labels.max() == 2 - # assert False From 18a6535089712d40dae3e168d30107e53a71921b Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 16 Jun 2014 23:07:37 +0530 Subject: [PATCH 0052/1122] Corrected test mistake --- skimage/graph/tests/test_rag.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 8c28febe..e239e79f 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -19,4 +19,5 @@ def test_threshold_cut(): rag = graph.rag_meancolor(img, labels) new_labels = graph.threshold_cut(labels, rag, 10) - assert new_labels.max() == 2 + # Two labels + assert new_labels.max() == 1 From bf89726c496256c9749b213222ef2357c338d604 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 16 Jun 2014 23:50:26 +0530 Subject: [PATCH 0053/1122] removed comments --- skimage/graph/graph_cut.py | 1 - skimage/graph/rag.py | 1 - 2 files changed, 2 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index d6e1d013..0e7ea25c 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -40,7 +40,6 @@ def threshold_cut(label, rag, thresh): out = np.copy(label) for i, nodes in enumerate(comps): - for node in nodes: for l in rag.node[node]['labels']: out[label == l] = i diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index af24af18..d4ad6628 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -32,7 +32,6 @@ class RAG(nx.Graph): if not self.has_edge(i, j): raise ValueError('Cant merge non adjacent nodes') - # print "before ",self.order() for x in self.neighbors(i): if x == j: continue From 47e3b6ea67b64e5cf1d8f9ef0bc1b6fe152e6951 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Tue, 17 Jun 2014 00:10:56 +0530 Subject: [PATCH 0054/1122] Added example --- doc/examples/plot_rag_meancolor.py | 41 ++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 doc/examples/plot_rag_meancolor.py diff --git a/doc/examples/plot_rag_meancolor.py b/doc/examples/plot_rag_meancolor.py new file mode 100644 index 00000000..aee341f4 --- /dev/null +++ b/doc/examples/plot_rag_meancolor.py @@ -0,0 +1,41 @@ +from skimage import graph +from skimage import segmentation +from skimage import data, io +import numpy as np +from matplotlib import pyplot as plt + + +def label_mask_img(img, label): + + out = np.zeros_like(img) + + red = img[:, :, 0] + green = img[:, :, 1] + blue = img[:, :, 2] + + for i in range(label.max()): + mask = label == i + + r = np.average(red[mask]) + g = np.average(green[mask]) + b = np.average(blue[mask]) + + # print r,g,b + out[mask] = r, g, b + + return out + +img = data.coffee() + +labels1 = segmentation.slic(img, compactness=30, n_segments=400) +out1 = label_mask_img(img, labels1) + +g = graph.rag_meancolor(img, labels1) +labels2 = graph.threshold_cut(labels1, g, 30) +out2 = label_mask_img(img, labels2) + +plt.figure() +io.imshow(out1) +plt.figure() +io.imshow(out2) +io.show() From 602c8d869217949aef5c3c024e9ad81c6b60e3e0 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Tue, 17 Jun 2014 00:17:12 +0530 Subject: [PATCH 0055/1122] Added explanation to example --- doc/examples/plot_rag_meancolor.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/examples/plot_rag_meancolor.py b/doc/examples/plot_rag_meancolor.py index aee341f4..988572b5 100644 --- a/doc/examples/plot_rag_meancolor.py +++ b/doc/examples/plot_rag_meancolor.py @@ -1,3 +1,14 @@ +""" +================ +RAG Thresholding +================ + +This examples constructs a Region Adjacency Graph and merges region which are +similar in color. We construct a RAG and define edges as the difference in +mean color. We the join regions with similar mean color. + +""" + from skimage import graph from skimage import segmentation from skimage import data, io From 076b044e4548f509f29231f470e1f03b7f0f06f1 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Tue, 17 Jun 2014 00:37:26 +0530 Subject: [PATCH 0056/1122] Added bento.info --- bento.info | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bento.info b/bento.info index 227e51e8..68aa4319 100644 --- a/bento.info +++ b/bento.info @@ -154,6 +154,9 @@ Library: Extension: skimage.feature._hessian_det_appx Sources: skimage/exposure/_hessian_det_appx.pyx + Extension: skimage.graph._construct + Sources: + skimage/exposure/_construct.pyx Executable: skivi Module: skimage.scripts.skivi From dfee0685508f264aad1703c1652ad2817f93e325 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Tue, 17 Jun 2014 00:59:22 +0530 Subject: [PATCH 0057/1122] Added networkx to requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 48e03b8b..7543d867 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ cython>=0.17 matplotlib>=1.0 numpy>=1.6 six>=1.3.0 +networkx>=1.8.0 From 5165ebe01ea5b5a93e5562890a772ff4995ce33d Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Tue, 17 Jun 2014 01:22:22 +0530 Subject: [PATCH 0058/1122] Added networkx installation --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 169b512b..1f292dfa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,6 +54,7 @@ before_install: - pip install cython - pip install flake8 - pip install six + - pip install networkx - pip install nose-cov - pip install coveralls From 53318c44dfcd7158b21a31346d362210c592aa7b Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Tue, 17 Jun 2014 22:36:48 +0530 Subject: [PATCH 0059/1122] CHanges to get Python3 working --- skimage/graph/_construct.pyx | 4 ++-- skimage/graph/rag.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/skimage/graph/_construct.pyx b/skimage/graph/_construct.pyx index c4fbb465..c3cde3b9 100644 --- a/skimage/graph/_construct.pyx +++ b/skimage/graph/_construct.pyx @@ -1,6 +1,6 @@ -import rag -cimport numpy as cnp import numpy as np +cimport numpy as cnp +import rag def construct_rag_meancolor_3d(img, arr): diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index d4ad6628..1129d4a9 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -1,6 +1,7 @@ import networkx as nx -import _construct from skimage import util +from ._construct import construct_rag_meancolor_2d +from ._construct import construct_rag_meancolor_3d class RAG(nx.Graph): @@ -80,8 +81,8 @@ def rag_meancolor(img, labels): img = util.img_as_ubyte(img) if img.ndim == 4: - return _construct.construct_rag_meancolor_3d(img, labels) + return construct_rag_meancolor_3d(img, labels) elif img.ndim == 3: - return _construct.construct_rag_meancolor_2d(img, labels) + return construct_rag_meancolor_2d(img, labels) else: raise ValueError("Image dimension not supported") From 2a7ec3afd42f807484be3077ef4ef8ed48ca5d02 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 18 Jun 2014 00:46:25 +0530 Subject: [PATCH 0060/1122] Renamed _construct.pyx to _build_rag.pyx and some code corrections --- doc/examples/plot_rag_meancolor.py | 4 +- .../graph/{_construct.pyx => _build_rag.pyx} | 68 +++++++++---------- skimage/graph/graph_cut.py | 2 +- skimage/graph/rag.py | 4 +- skimage/graph/setup.py | 4 +- 5 files changed, 41 insertions(+), 41 deletions(-) rename skimage/graph/{_construct.pyx => _build_rag.pyx} (64%) diff --git a/doc/examples/plot_rag_meancolor.py b/doc/examples/plot_rag_meancolor.py index 988572b5..aebfe6fc 100644 --- a/doc/examples/plot_rag_meancolor.py +++ b/doc/examples/plot_rag_meancolor.py @@ -3,9 +3,9 @@ RAG Thresholding ================ -This examples constructs a Region Adjacency Graph and merges region which are +This examples constructs a Region Adjacency Graph (RAG) and merges regions which are similar in color. We construct a RAG and define edges as the difference in -mean color. We the join regions with similar mean color. +mean color. We then join regions with similar mean color. """ diff --git a/skimage/graph/_construct.pyx b/skimage/graph/_build_rag.pyx similarity index 64% rename from skimage/graph/_construct.pyx rename to skimage/graph/_build_rag.pyx index c3cde3b9..e6825c9d 100644 --- a/skimage/graph/_construct.pyx +++ b/skimage/graph/_build_rag.pyx @@ -8,9 +8,9 @@ def construct_rag_meancolor_3d(img, arr): difference in mean color of regions as edge weights. Given an image and its segmentation, this method constructs the - corresponsing Region Adjacency Graph (RAG).Each node in the RAG - represents a contiguous pixels with in `img` the same label in - `arr` + corresponsing Region Adjacency Graph (RAG). Each node in the RAG + represents contiguous pixels with in `img` with the same label in + `arr`. There is an edge between each pair of adjacent regions. Parameters ---------- @@ -25,29 +25,29 @@ def construct_rag_meancolor_3d(img, arr): The region adjacency graph. """ - cdef Py_ssize_t l, b, h, i, j, k + cdef Py_ssize_t depth,width,height, i, j, k cdef cnp.int32_t current, next - l = arr.shape[0] - b = arr.shape[1] - h = arr.shape[2] + width = arr.shape[0] + height = arr.shape[1] + depth = arr.shape[2] g = rag.RAG() i = 0 - while i < l - 1: + for i in range(width-1): j = 0 - while j < b - 1: + for j in range(height-1): k = 0 - while k < h - 1: + for k in range(depth-1): current = arr[i, j, k] try: - g.node[current]['pixel_count'] += 1 - g.node[current]['total_color'] += img[i, j] + g.node[current]['pixel count'] += 1 + g.node[current]['total color'] += img[i, j] except KeyError: g.add_node(current) - g.node[current]['pixel_count'] = 1 - g.node[current]['total_color'] = img[i, j].astype(np.long) + g.node[current]['pixel count'] = 1 + g.node[current]['total color'] = img[i, j].astype(np.long) g.node[current]['labels'] = [arr[i, j]] next = arr[i + 1, j, k] @@ -85,12 +85,12 @@ def construct_rag_meancolor_3d(img, arr): i += 1 for n in g.nodes(): - g.node[n]['mean_color'] = g.node[n][ - 'total_color'] / g.node[n]['pixel_count'] + g.node[n]['mean color'] = g.node[n][ + 'total color'] / g.node[n]['pixel count'] for x, y in g.edges_iter(): - diff = g.node[x]['mean_color'] - g.node[y]['mean_color'] - g[x][y]['weight'] = np.sqrt(diff.dot(diff)) + diff = g.node[x]['mean color'] - g.node[y]['mean color'] + g[x][y]['weight'] = np.linalg.norm(diff) return g @@ -100,9 +100,9 @@ def construct_rag_meancolor_2d(img, arr): difference in mean color of regions as edge weights. Given an image and its segmentation, this method constructs the - corresponsing Region Adjacency Graph (RAG).Each node in the RAG - represents a contiguous pixels with in `img` the same label in - `arr` + corresponsing Region Adjacency Graph (RAG). Each node in the RAG + represents contiguous pixels with in `img` with the same label in + `arr`. There is an edge between each pair of adjacent regions. Parameters ---------- @@ -117,26 +117,26 @@ def construct_rag_meancolor_2d(img, arr): The region adjacency graph. """ - cdef Py_ssize_t l, b, h, i, j, k + cdef Py_ssize_t width, height, h, i, j, k cdef cnp.int32_t current, next - l = arr.shape[0] - b = arr.shape[1] + width = arr.shape[0] + height = arr.shape[1] g = rag.RAG() i = 0 - while i < l - 1: + for i in range(width-1): j = 0 - while j < b - 1: + for j in range(height-1): current = arr[i, j] try: - g.node[current]['pixel_count'] += 1 - g.node[current]['total_color'] += img[i, j] + g.node[current]['pixel count'] += 1 + g.node[current]['total color'] += img[i, j] except KeyError: g.add_node(current) - g.node[current]['pixel_count'] = 1 - g.node[current]['total_color'] = img[i, j].astype(np.long) + g.node[current]['pixel count'] = 1 + g.node[current]['total color'] = img[i, j].astype(np.long) g.node[current]['labels'] = [arr[i, j]] next = arr[i + 1, j] @@ -156,11 +156,11 @@ def construct_rag_meancolor_2d(img, arr): i += 1 for n in g.nodes(): - g.node[n]['mean_color'] = g.node[n][ - 'total_color'] / g.node[n]['pixel_count'] + g.node[n]['mean color'] = g.node[n][ + 'total color'] / g.node[n]['pixel count'] for x, y in g.edges_iter(): - diff = g.node[x]['mean_color'] - g.node[y]['mean_color'] - g[x][y]['weight'] = np.sqrt(diff.dot(diff)) + diff = g.node[x]['mean color'] - g.node[y]['mean color'] + g[x][y]['weight'] = np.linalg.norm(diff) return g diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 0e7ea25c..40adcdf8 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -11,7 +11,7 @@ def threshold_cut(label, rag, thresh): Parameters ---------- - label : (width, height, 3) or (width, height, depth, 3) ndarray + label : (width, height) or (width, height, 3) ndarray The array of labels. rag : RAG The region adjacency graph. diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 1129d4a9..72befba9 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -1,7 +1,7 @@ import networkx as nx from skimage import util -from ._construct import construct_rag_meancolor_2d -from ._construct import construct_rag_meancolor_3d +from ._build_rag import construct_rag_meancolor_2d +from ._build_rag import construct_rag_meancolor_3d class RAG(nx.Graph): diff --git a/skimage/graph/setup.py b/skimage/graph/setup.py index 252eeed2..436029da 100644 --- a/skimage/graph/setup.py +++ b/skimage/graph/setup.py @@ -17,7 +17,7 @@ def configuration(parent_package='', top_path=None): cython(['_spath.pyx'], working_path=base_path) cython(['_mcp.pyx'], working_path=base_path) cython(['heap.pyx'], working_path=base_path) - cython(['_construct.pyx'], working_path=base_path) + cython(['_build_rag.pyx'], working_path=base_path) config.add_extension('_spath', sources=['_spath.c'], @@ -26,7 +26,7 @@ def configuration(parent_package='', top_path=None): include_dirs=[get_numpy_include_dirs()]) config.add_extension('heap', sources=['heap.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('_construct', sources=['_construct.c'], + config.add_extension('_build_rag', sources=['_build_rag.c'], include_dirs=[get_numpy_include_dirs()]) From f5f93f8ce32b53b24c1ec1d76692a60b5ac86f8d Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 18 Jun 2014 03:53:33 +0530 Subject: [PATCH 0061/1122] Removed cython file and added nd python function for RAG construction --- skimage/graph/_build_rag.pyx | 166 ----------------------------------- skimage/graph/rag.py | 65 +++++++++++--- skimage/graph/setup.py | 4 - 3 files changed, 54 insertions(+), 181 deletions(-) delete mode 100644 skimage/graph/_build_rag.pyx diff --git a/skimage/graph/_build_rag.pyx b/skimage/graph/_build_rag.pyx deleted file mode 100644 index e6825c9d..00000000 --- a/skimage/graph/_build_rag.pyx +++ /dev/null @@ -1,166 +0,0 @@ -import numpy as np -cimport numpy as cnp -import rag - - -def construct_rag_meancolor_3d(img, arr): - """Computes the Region Adjacency Graph of a 3D color image using - difference in mean color of regions as edge weights. - - Given an image and its segmentation, this method constructs the - corresponsing Region Adjacency Graph (RAG). Each node in the RAG - represents contiguous pixels with in `img` with the same label in - `arr`. There is an edge between each pair of adjacent regions. - - Parameters - ---------- - img : (width, height, depth, 3) ndarray - Input image. - arr : (width, height, depth) ndarray - The array with labels. - - Returns - ------- - out : RAG - The region adjacency graph. - """ - - cdef Py_ssize_t depth,width,height, i, j, k - cdef cnp.int32_t current, next - width = arr.shape[0] - height = arr.shape[1] - depth = arr.shape[2] - - g = rag.RAG() - - i = 0 - for i in range(width-1): - j = 0 - for j in range(height-1): - k = 0 - for k in range(depth-1): - current = arr[i, j, k] - - try: - g.node[current]['pixel count'] += 1 - g.node[current]['total color'] += img[i, j] - except KeyError: - g.add_node(current) - g.node[current]['pixel count'] = 1 - g.node[current]['total color'] = img[i, j].astype(np.long) - g.node[current]['labels'] = [arr[i, j]] - - next = arr[i + 1, j, k] - if current != next: - g.add_edge(current, next) - - next = arr[i, j + 1, k] - if current != next: - g.add_edge(current, next) - - next = arr[i + 1, j + 1, k] - if current != next: - g.add_edge(current, next) - - next = arr[i + 1, j, k + 1] - if current != next: - g.add_edge(current, next) - - next = arr[i, j + 1, k + 1] - if current != next: - g.add_edge(current, next) - - next = arr[i + 1, j + 1, k + 1] - if current != next: - g.add_edge(current, next) - - next = arr[i, j, k + 1] - if current != next: - g.add_edge(current, next) - - k += 1 - - j += 1 - - i += 1 - - for n in g.nodes(): - g.node[n]['mean color'] = g.node[n][ - 'total color'] / g.node[n]['pixel count'] - - for x, y in g.edges_iter(): - diff = g.node[x]['mean color'] - g.node[y]['mean color'] - g[x][y]['weight'] = np.linalg.norm(diff) - - return g - - -def construct_rag_meancolor_2d(img, arr): - """Computes the Region Adjacency Graph of a 2D color image using - difference in mean color of regions as edge weights. - - Given an image and its segmentation, this method constructs the - corresponsing Region Adjacency Graph (RAG). Each node in the RAG - represents contiguous pixels with in `img` with the same label in - `arr`. There is an edge between each pair of adjacent regions. - - Parameters - ---------- - img : (width, height, 3) ndarray - Input image. - arr : (width, height) ndarray - The array with labels. - - Returns - ------- - out : RAG - The region adjacency graph. - """ - - cdef Py_ssize_t width, height, h, i, j, k - cdef cnp.int32_t current, next - width = arr.shape[0] - height = arr.shape[1] - - g = rag.RAG() - - i = 0 - for i in range(width-1): - j = 0 - for j in range(height-1): - current = arr[i, j] - - try: - g.node[current]['pixel count'] += 1 - g.node[current]['total color'] += img[i, j] - except KeyError: - g.add_node(current) - g.node[current]['pixel count'] = 1 - g.node[current]['total color'] = img[i, j].astype(np.long) - g.node[current]['labels'] = [arr[i, j]] - - next = arr[i + 1, j] - if current != next: - g.add_edge(current, next) - - next = arr[i, j + 1] - if current != next: - g.add_edge(current, next) - - next = arr[i + 1, j + 1] - if current != next: - g.add_edge(current, next) - - j += 1 - - i += 1 - - for n in g.nodes(): - g.node[n]['mean color'] = g.node[n][ - 'total color'] / g.node[n]['pixel count'] - - for x, y in g.edges_iter(): - diff = g.node[x]['mean color'] - g.node[y]['mean color'] - g[x][y]['weight'] = np.linalg.norm(diff) - - return g diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 72befba9..92231280 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -1,7 +1,7 @@ import networkx as nx -from skimage import util -from ._build_rag import construct_rag_meancolor_2d -from ._build_rag import construct_rag_meancolor_3d +from . import rag +import numpy as np +from scipy.ndimage import filters class RAG(nx.Graph): @@ -49,7 +49,18 @@ class RAG(nx.Graph): self.remove_node(i) -def rag_meancolor(img, labels): +def _add_edge(values, g): + values = values.astype(int) + current = values[0] + + for value in values[1:]: + if value >= 0: + g.add_edge(current, value) + + return 0.0 + + +def rag_mean_color(img, arr): """Computes the Region Adjacency Graph of a color image using difference in mean color of regions as edge weights. @@ -78,11 +89,43 @@ def rag_meancolor(img, labels): >>> rag = graph.rag_meancolor(img, labels) """ + g = rag.RAG() - img = util.img_as_ubyte(img) - if img.ndim == 4: - return construct_rag_meancolor_3d(img, labels) - elif img.ndim == 3: - return construct_rag_meancolor_2d(img, labels) - else: - raise ValueError("Image dimension not supported") + fp = np.zeros((3,) * arr.ndim) + slc = slice(1, None, None) + fp[(slc,) * arr.ndim] = 1 + + filters.generic_filter( + arr, + function=_add_edge, + footprint=fp, + mode='constant', + cval=-1, + extra_arguments=(g, + )) + iter = np.nditer(arr, flags=['multi_index']) + + while not iter.finished: + + current = arr[iter.multi_index] + try: + g.node[current]['pixel count'] += 1 + g.node[current]['total color'] += img[iter.multi_index] + except KeyError: + g.add_node(current) + g.node[current]['pixel count'] = 1 + g.node[current]['total color'] = img[ + iter.multi_index].astype(np.long) + g.node[current]['labels'] = [arr[iter.multi_index]] + + iter.iternext() + + for n in g.nodes(): + g.node[n]['mean color'] = g.node[n][ + 'total color'] / g.node[n]['pixel count'] + + for x, y in g.edges_iter(): + diff = g.node[x]['mean color'] - g.node[y]['mean color'] + g[x][y]['weight'] = np.linalg.norm(diff) + + return g diff --git a/skimage/graph/setup.py b/skimage/graph/setup.py index 436029da..45dd205a 100644 --- a/skimage/graph/setup.py +++ b/skimage/graph/setup.py @@ -17,8 +17,6 @@ def configuration(parent_package='', top_path=None): cython(['_spath.pyx'], working_path=base_path) cython(['_mcp.pyx'], working_path=base_path) cython(['heap.pyx'], working_path=base_path) - cython(['_build_rag.pyx'], working_path=base_path) - config.add_extension('_spath', sources=['_spath.c'], include_dirs=[get_numpy_include_dirs()]) @@ -26,8 +24,6 @@ def configuration(parent_package='', top_path=None): include_dirs=[get_numpy_include_dirs()]) config.add_extension('heap', sources=['heap.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('_build_rag', sources=['_build_rag.c'], - include_dirs=[get_numpy_include_dirs()]) return config From 9b8e1333e838ace440a349d54c0fe1cf00731cd7 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 18 Jun 2014 03:59:12 +0530 Subject: [PATCH 0062/1122] Corrected rag import --- skimage/graph/rag.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 92231280..e788455d 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -1,5 +1,4 @@ import networkx as nx -from . import rag import numpy as np from scipy.ndimage import filters @@ -60,7 +59,7 @@ def _add_edge(values, g): return 0.0 -def rag_mean_color(img, arr): +def rag_meancolor(img, arr): """Computes the Region Adjacency Graph of a color image using difference in mean color of regions as edge weights. @@ -89,7 +88,7 @@ def rag_mean_color(img, arr): >>> rag = graph.rag_meancolor(img, labels) """ - g = rag.RAG() + g = RAG() fp = np.zeros((3,) * arr.ndim) slc = slice(1, None, None) From 78397bf54dc5f8598cd7cf2ea5ebeb2f050e9aca Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 19 Jun 2014 00:05:14 +0530 Subject: [PATCH 0063/1122] reverted benfo.info and setup.py --- bento.info | 3 --- skimage/graph/setup.py | 1 - 2 files changed, 4 deletions(-) diff --git a/bento.info b/bento.info index 68aa4319..227e51e8 100644 --- a/bento.info +++ b/bento.info @@ -154,9 +154,6 @@ Library: Extension: skimage.feature._hessian_det_appx Sources: skimage/exposure/_hessian_det_appx.pyx - Extension: skimage.graph._construct - Sources: - skimage/exposure/_construct.pyx Executable: skivi Module: skimage.scripts.skivi diff --git a/skimage/graph/setup.py b/skimage/graph/setup.py index 45dd205a..463d2739 100644 --- a/skimage/graph/setup.py +++ b/skimage/graph/setup.py @@ -25,7 +25,6 @@ def configuration(parent_package='', top_path=None): config.add_extension('heap', sources=['heap.c'], include_dirs=[get_numpy_include_dirs()]) - return config if __name__ == '__main__': From 53330496f8965b210b97be6717184fa01a02dc89 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 19 Jun 2014 01:11:26 +0530 Subject: [PATCH 0064/1122] Added references --- skimage/graph/graph_cut.py | 7 +++++++ skimage/graph/rag.py | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 40adcdf8..2e72ac2b 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -31,6 +31,13 @@ def threshold_cut(label, rag, thresh): >>> labels = segmentation.slic(img) >>> rag = graph.rag_meancolor(img, labels) >>> new_labels = graph.threshold_cut(labels, rag, 10) + + References + ---------- + .. [1] Alain Tremeau and Philippe Colantoni + "Regions Adjacency Graph Applied To Color Image Segmentation" + http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.11.5274 + """ to_remove = [(x, y) for x, y, d in rag.edges_iter(data=True) if d['weight'] >= thresh] diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index e788455d..0b01880d 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -66,7 +66,7 @@ def rag_meancolor(img, arr): Given an image and its segmentation, this method constructs the corresponsing Region Adjacency Graph (RAG).Each node in the RAG represents a contiguous pixels with in `img` the same label in - `arr` + `arr`. Parameters ---------- @@ -87,6 +87,12 @@ def rag_meancolor(img, arr): >>> labels = segmentation.slic(img) >>> rag = graph.rag_meancolor(img, labels) + References + ---------- + .. [1] Alain Tremeau and Philippe Colantoni + "Regions Adjacency Graph Applied To Color Image Segmentation" + http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.11.5274 + """ g = RAG() From 410aecb354e5ce72c05979b088defcfeab5c131e Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 19 Jun 2014 02:49:02 +0530 Subject: [PATCH 0065/1122] type casting to double instead of long --- skimage/graph/rag.py | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 0b01880d..74bed4ad 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -14,7 +14,7 @@ class RAG(nx.Graph): """ def merge_nodes(self, i, j): - """Merges nodes `i` and `j`. + """Merge node `i` into `j`. The new combined node is adjacent to all the neighbors of `i` and `j`. In case of conflicting edges, edge with higher weight @@ -48,7 +48,23 @@ class RAG(nx.Graph): self.remove_node(i) -def _add_edge(values, g): +def _add_edge_filter(values, g): + """Adds an edge between first element in `values` and + all other elements of ` in the graph `g`. + + Paramteres + ---------- + values : array + The array to process. + g : RAG + The graph to add edges in. + + Returns + ------- + 0.0 : float + Always returns 0. + + """ values = values.astype(int) current = values[0] @@ -102,32 +118,27 @@ def rag_meancolor(img, arr): filters.generic_filter( arr, - function=_add_edge, + function=_add_edge_filter, footprint=fp, mode='constant', cval=-1, extra_arguments=(g, )) - iter = np.nditer(arr, flags=['multi_index']) - while not iter.finished: - - current = arr[iter.multi_index] + for index in np.ndindex(arr.shape): + current = arr[index] try: g.node[current]['pixel count'] += 1 - g.node[current]['total color'] += img[iter.multi_index] + g.node[current]['total color'] += img[index] except KeyError: g.add_node(current) g.node[current]['pixel count'] = 1 - g.node[current]['total color'] = img[ - iter.multi_index].astype(np.long) - g.node[current]['labels'] = [arr[iter.multi_index]] + g.node[current]['total color'] = img[index].astype(np.double) + g.node[current]['labels'] = [arr[index]] - iter.iternext() - - for n in g.nodes(): - g.node[n]['mean color'] = g.node[n][ - 'total color'] / g.node[n]['pixel count'] + for n in g: + g.node[n]['mean color'] = (g.node[n]['total color'] / + g.node[n]['pixel count']) for x, y in g.edges_iter(): diff = g.node[x]['mean color'] - g.node[y]['mean color'] From 8a36040eb88a9c55243a63ccf9564aa97209ab1f Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 19 Jun 2014 02:54:20 +0530 Subject: [PATCH 0066/1122] rebased and used color.label2rgb --- doc/examples/plot_rag_meancolor.py | 32 ++++++------------------------ 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/doc/examples/plot_rag_meancolor.py b/doc/examples/plot_rag_meancolor.py index aebfe6fc..1f19dd5e 100644 --- a/doc/examples/plot_rag_meancolor.py +++ b/doc/examples/plot_rag_meancolor.py @@ -3,47 +3,27 @@ RAG Thresholding ================ -This examples constructs a Region Adjacency Graph (RAG) and merges regions which are -similar in color. We construct a RAG and define edges as the difference in -mean color. We then join regions with similar mean color. +This examples constructs a Region Adjacency Graph (RAG) and merges regions +which are similar in color. We construct a RAG and define edges as the +difference in mean color. We then join regions with similar mean color. """ from skimage import graph from skimage import segmentation from skimage import data, io -import numpy as np from matplotlib import pyplot as plt +from skimage import color -def label_mask_img(img, label): - - out = np.zeros_like(img) - - red = img[:, :, 0] - green = img[:, :, 1] - blue = img[:, :, 2] - - for i in range(label.max()): - mask = label == i - - r = np.average(red[mask]) - g = np.average(green[mask]) - b = np.average(blue[mask]) - - # print r,g,b - out[mask] = r, g, b - - return out - img = data.coffee() labels1 = segmentation.slic(img, compactness=30, n_segments=400) -out1 = label_mask_img(img, labels1) +out1 = color.label2rgb(labels1, img, kind='avg') g = graph.rag_meancolor(img, labels1) labels2 = graph.threshold_cut(labels1, g, 30) -out2 = label_mask_img(img, labels2) +out2 = color.label2rgb(labels2, img, kind='avg') plt.figure() io.imshow(out1) From 20cefcf50a31df3bd820492521abec2fbd752054 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 19 Jun 2014 16:18:53 +0530 Subject: [PATCH 0067/1122] Added comments and switched try with if --- skimage/graph/graph_cut.py | 1 + skimage/graph/rag.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 2e72ac2b..a255d01e 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -39,6 +39,7 @@ def threshold_cut(label, rag, thresh): http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.11.5274 """ + # Because deleting edges while iterating through them produces an error. to_remove = [(x, y) for x, y, d in rag.edges_iter(data=True) if d['weight'] >= thresh] rag.remove_edges_from(to_remove) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 74bed4ad..92f8c62e 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -50,9 +50,9 @@ class RAG(nx.Graph): def _add_edge_filter(values, g): """Adds an edge between first element in `values` and - all other elements of ` in the graph `g`. + all other elements of `values` in the graph `g`. - Paramteres + Parameters ---------- values : array The array to process. @@ -116,22 +116,24 @@ def rag_meancolor(img, arr): slc = slice(1, None, None) fp[(slc,) * arr.ndim] = 1 + # The footprint is constructed in such a way that the first + # element in the array being passed to _add_edge_filter is + # the central value. filters.generic_filter( arr, function=_add_edge_filter, footprint=fp, mode='constant', cval=-1, - extra_arguments=(g, - )) + extra_arguments=(g,)) for index in np.ndindex(arr.shape): current = arr[index] - try: + + if 'pixel count' in g.node[current]: g.node[current]['pixel count'] += 1 g.node[current]['total color'] += img[index] - except KeyError: - g.add_node(current) + else: g.node[current]['pixel count'] = 1 g.node[current]['total color'] = img[index].astype(np.double) g.node[current]['labels'] = [arr[index]] From d0e7a26f6fcb19013e018cf11f1137ffb0a19b7d Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 19 Jun 2014 16:36:17 +0530 Subject: [PATCH 0068/1122] Added RAG merge_nodes test case --- skimage/graph/tests/test_rag.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index e239e79f..39e99852 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -1,5 +1,22 @@ import numpy as np from skimage import graph +import random + + +def test_rag_merge(): + g = graph.rag.RAG() + for i in range(10): + g.add_edge(i, (i + 1) % 10, {'weight': i * 10}) + g.node[i]['labels'] = [i] + + for i in range(9): + x = random.choice(g.nodes()) + y = random.choice(g.neighbors(x)) + g.merge_nodes(x, y) + + idx = g.nodes()[0] + sorted(g.node[idx]['labels']) == range(10) + assert g.edges() == [] def test_threshold_cut(): From 76d3dbad57c305b3881e074d7caed0236b5420ef Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 19 Jun 2014 17:57:07 +0530 Subject: [PATCH 0069/1122] added merge_nodes test --- skimage/graph/tests/test_rag.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 39e99852..f923de2e 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -3,6 +3,7 @@ from skimage import graph import random + def test_rag_merge(): g = graph.rag.RAG() for i in range(10): @@ -14,6 +15,8 @@ def test_rag_merge(): y = random.choice(g.neighbors(x)) g.merge_nodes(x, y) + np.testing.assert_raises(ValueError,g.merge_nodes,7,9) + idx = g.nodes()[0] sorted(g.node[idx]['labels']) == range(10) assert g.edges() == [] From 711de464e51e0315f76905e24023a7f2902b8386 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 19 Jun 2014 18:03:44 +0530 Subject: [PATCH 0070/1122] Formatting changes --- skimage/graph/tests/test_rag.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index f923de2e..6ac1c373 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -3,7 +3,6 @@ from skimage import graph import random - def test_rag_merge(): g = graph.rag.RAG() for i in range(10): @@ -15,7 +14,7 @@ def test_rag_merge(): y = random.choice(g.neighbors(x)) g.merge_nodes(x, y) - np.testing.assert_raises(ValueError,g.merge_nodes,7,9) + np.testing.assert_raises(ValueError, g.merge_nodes, 7, 9) idx = g.nodes()[0] sorted(g.node[idx]['labels']) == range(10) From 0e35c422b398f737f96264caa34e0543f60e3065 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sun, 22 Jun 2014 13:58:00 +0530 Subject: [PATCH 0071/1122] Graph can now merge non adjacent nodes --- skimage/graph/rag.py | 5 ----- skimage/graph/tests/test_rag.py | 12 ++++++------ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 92f8c62e..8bcb06b9 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -28,10 +28,6 @@ class RAG(nx.Graph): Node to be merged. """ - - if not self.has_edge(i, j): - raise ValueError('Cant merge non adjacent nodes') - for x in self.neighbors(i): if x == j: continue @@ -41,7 +37,6 @@ class RAG(nx.Graph): w2 = self.get_edge_data(x, j)['weight'] w = max(w1, w2) - self.add_edge(x, j, weight=w) self.node[j]['labels'] += self.node[i]['labels'] diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 6ac1c373..1225e774 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -11,13 +11,13 @@ def test_rag_merge(): for i in range(9): x = random.choice(g.nodes()) - y = random.choice(g.neighbors(x)) - g.merge_nodes(x, y) - - np.testing.assert_raises(ValueError, g.merge_nodes, 7, 9) - + y = random.choice(g.nodes()) + while x == y : + y = random.choice(g.nodes()) + g.merge_nodes(x,y) + idx = g.nodes()[0] - sorted(g.node[idx]['labels']) == range(10) + assert sorted(g.node[idx]['labels']) == range(10) assert g.edges() == [] From 319530fb8922185efb645acd364b046078b4bc92 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sun, 22 Jun 2014 19:48:52 +0530 Subject: [PATCH 0072/1122] Used mapping in graph_cut.py --- skimage/graph/graph_cut.py | 12 ++++++------ skimage/graph/rag.py | 9 ++++++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index a255d01e..27e4400d 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -2,7 +2,7 @@ import networkx as nx import numpy as np -def threshold_cut(label, rag, thresh): +def threshold_cut(label_image, rag, thresh): """Combines regions seperated by weight less than threshold. Given an image's labels and its RAG, outputs new labels by @@ -11,7 +11,7 @@ def threshold_cut(label, rag, thresh): Parameters ---------- - label : (width, height) or (width, height, 3) ndarray + label_image : (width, height) or (width, height, 3) ndarray The array of labels. rag : RAG The region adjacency graph. @@ -45,11 +45,11 @@ def threshold_cut(label, rag, thresh): rag.remove_edges_from(to_remove) comps = nx.connected_components(rag) - out = np.copy(label) + map_array = np.arange(label_image.max()+1, dtype = np.int) for i, nodes in enumerate(comps): for node in nodes: - for l in rag.node[node]['labels']: - out[label == l] = i + for label in rag.node[node]['labels']: + map_array[label] = i - return out + return map_array[label_image] diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 8bcb06b9..e2f60195 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -13,12 +13,12 @@ class RAG(nx.Graph): between their corresponding nodes. """ - def merge_nodes(self, i, j): + def merge_nodes(self, i, j, function=max): """Merge node `i` into `j`. The new combined node is adjacent to all the neighbors of `i` - and `j`. In case of conflicting edges, edge with higher weight - is chosen. + and `j`. In case of conflicting edges the given function is + called. Parameters ---------- @@ -26,6 +26,9 @@ class RAG(nx.Graph): Node to be merged. j : int Node to be merged. + function : callable, optional + Function to decide which edge weight to keep when a node is + adjacent to both `i` and `j`. """ for x in self.neighbors(i): From 3db0a2f81fa939673b6f1755427fbded8c5323f8 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sun, 22 Jun 2014 20:27:44 +0530 Subject: [PATCH 0073/1122] Naming changes and formatting --- skimage/graph/rag.py | 42 ++++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index e2f60195..3217bda2 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -4,12 +4,11 @@ from scipy.ndimage import filters class RAG(nx.Graph): - """ The class for holding the Region Adjacency Graph (RAG). - Each region is a contiguous set of pixels in an image, usuall - sharing some common property.Adjacent regions have an edge + Each region is a contiguous set of pixels in an image, usually + sharing some common property. Adjacent regions have an edge between their corresponding nodes. """ @@ -22,14 +21,11 @@ class RAG(nx.Graph): Parameters ---------- - i : int - Node to be merged. - j : int - Node to be merged. + i, j : int + Nodes to be merged. The resulting node will have ID `j`. function : callable, optional Function to decide which edge weight to keep when a node is adjacent to both `i` and `j`. - """ for x in self.neighbors(i): if x == j: @@ -38,7 +34,6 @@ class RAG(nx.Graph): w2 = -1 if self.has_edge(x, j): w2 = self.get_edge_data(x, j)['weight'] - w = max(w1, w2) self.add_edge(x, j, weight=w) @@ -47,7 +42,7 @@ class RAG(nx.Graph): def _add_edge_filter(values, g): - """Adds an edge between first element in `values` and + """Add an edge between first element in `values` and all other elements of `values` in the graph `g`. Parameters @@ -65,7 +60,6 @@ def _add_edge_filter(values, g): """ values = values.astype(int) current = values[0] - for value in values[1:]: if value >= 0: g.add_edge(current, value) @@ -73,20 +67,20 @@ def _add_edge_filter(values, g): return 0.0 -def rag_meancolor(img, arr): - """Computes the Region Adjacency Graph of a color image using +def rag_meancolor(image, label_image): + """Compute the Region Adjacency Graph of a color image using difference in mean color of regions as edge weights. Given an image and its segmentation, this method constructs the - corresponsing Region Adjacency Graph (RAG).Each node in the RAG + corresponsing Region Adjacency Graph (RAG). Each node in the RAG represents a contiguous pixels with in `img` the same label in `arr`. Parameters ---------- - img : (width, height, 3) or (width, height, depth, 3) ndarray + image : (width, height, 3) or (width, height, depth, 3) ndarray Input image. - arr : (width, height) or (width, height, depth) ndarray + label_image : (width, height) or (width, height, depth) ndarray The array with labels. Returns @@ -110,31 +104,31 @@ def rag_meancolor(img, arr): """ g = RAG() - fp = np.zeros((3,) * arr.ndim) + fp = np.zeros((3,) * label_image.ndim) slc = slice(1, None, None) - fp[(slc,) * arr.ndim] = 1 + fp[(slc,) * label_image.ndim] = 1 # The footprint is constructed in such a way that the first # element in the array being passed to _add_edge_filter is # the central value. filters.generic_filter( - arr, + label_image, function=_add_edge_filter, footprint=fp, mode='constant', cval=-1, extra_arguments=(g,)) - for index in np.ndindex(arr.shape): - current = arr[index] + for index in np.ndindex(label_image.shape): + current = label_image[index] if 'pixel count' in g.node[current]: g.node[current]['pixel count'] += 1 - g.node[current]['total color'] += img[index] + g.node[current]['total color'] += image[index] else: g.node[current]['pixel count'] = 1 - g.node[current]['total color'] = img[index].astype(np.double) - g.node[current]['labels'] = [arr[index]] + g.node[current]['total color'] = image[index].astype(np.double) + g.node[current]['labels'] = [current] for n in g: g.node[n]['mean color'] = (g.node[n]['total color'] / From 0efca407477e6aeb87047824208251594d722463 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 23 Jun 2014 00:58:13 +0530 Subject: [PATCH 0074/1122] Added connectivity parameter and a function input to merge_nodes --- skimage/graph/rag.py | 43 ++++++++++++++++++++++++--------- skimage/graph/tests/test_rag.py | 13 +++++++++- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 3217bda2..dcda853b 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -1,6 +1,7 @@ import networkx as nx import numpy as np from scipy.ndimage import filters +from scipy import ndimage as nd class RAG(nx.Graph): @@ -12,7 +13,7 @@ class RAG(nx.Graph): between their corresponding nodes. """ - def merge_nodes(self, i, j, function=max): + def merge_nodes(self, i, j, function=None, extra_arguments=[], extra_keywords={}): """Merge node `i` into `j`. The new combined node is adjacent to all the neighbors of `i` @@ -25,7 +26,15 @@ class RAG(nx.Graph): Nodes to be merged. The resulting node will have ID `j`. function : callable, optional Function to decide which edge weight to keep when a node is - adjacent to both `i` and `j`. + adjacent to both `i` and `j`. The arguments passed to the + function are, the tuples represnting both the conflicting edges + and the graph.The default behaviour is that the edge with higher + weight is kept. + extra_arguments : sequence, optional + The sequence of extra positional arguments passed to + `function` + extra_keywords : + The dict of keyword arguments passed to the `function`. """ for x in self.neighbors(i): if x == j: @@ -34,7 +43,13 @@ class RAG(nx.Graph): w2 = -1 if self.has_edge(x, j): w2 = self.get_edge_data(x, j)['weight'] - w = max(w1, w2) + + w = w1 + if w2 > 0 : + if not function : + w = max(w1, w2) + else: + w = function((i, x), (j,x), self, *extra_arguments, **extra_keywords) self.add_edge(x, j, weight=w) self.node[j]['labels'] += self.node[i]['labels'] @@ -43,7 +58,8 @@ class RAG(nx.Graph): def _add_edge_filter(values, g): """Add an edge between first element in `values` and - all other elements of `values` in the graph `g`. + all other elements of `values` in the graph `g`.`values[0]` + is expected to be the central value of the footprint used. Parameters ---------- @@ -61,13 +77,12 @@ def _add_edge_filter(values, g): values = values.astype(int) current = values[0] for value in values[1:]: - if value >= 0: - g.add_edge(current, value) + g.add_edge(current, value) return 0.0 -def rag_meancolor(image, label_image): +def rag_meancolor(image, label_image, connectivity = 2): """Compute the Region Adjacency Graph of a color image using difference in mean color of regions as edge weights. @@ -82,6 +97,9 @@ def rag_meancolor(image, label_image): Input image. label_image : (width, height) or (width, height, depth) ndarray The array with labels. + connectivity : float, optional + Pixels with a squared distance less than `connectivity`from each other + are considered adjacent. Returns ------- @@ -104,9 +122,11 @@ def rag_meancolor(image, label_image): """ g = RAG() - fp = np.zeros((3,) * label_image.ndim) - slc = slice(1, None, None) - fp[(slc,) * label_image.ndim] = 1 + fp = nd.generate_binary_structure(label_image.ndim, connectivity) + for d in range(fp.ndim): + fp = fp.swapaxes(0, d) + fp[0, ...] = 0 + fp = fp.swapaxes(0, d) # The footprint is constructed in such a way that the first # element in the array being passed to _add_edge_filter is @@ -115,8 +135,7 @@ def rag_meancolor(image, label_image): label_image, function=_add_edge_filter, footprint=fp, - mode='constant', - cval=-1, + mode='nearest', extra_arguments=(g,)) for index in np.ndindex(label_image.shape): diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 1225e774..c644560c 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -2,6 +2,10 @@ import numpy as np from skimage import graph import random +def _min_edge((a1,b1),(a2,b2),g): + w1 = g.edge[a1][b1]['weight'] + w2 = g.edge[a2][b2]['weight'] + return min(w1,w2) def test_rag_merge(): g = graph.rag.RAG() @@ -9,13 +13,20 @@ def test_rag_merge(): g.add_edge(i, (i + 1) % 10, {'weight': i * 10}) g.node[i]['labels'] = [i] - for i in range(9): + for i in range(4): x = random.choice(g.nodes()) y = random.choice(g.nodes()) while x == y : y = random.choice(g.nodes()) g.merge_nodes(x,y) + for i in range(5): + x = random.choice(g.nodes()) + y = random.choice(g.nodes()) + while x == y : + y = random.choice(g.nodes()) + g.merge_nodes(x,y,_min_edge) + idx = g.nodes()[0] assert sorted(g.node[idx]['labels']) == range(10) assert g.edges() == [] From a1bc2e95fd506a3fa343984d00fe0061410cad75 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 23 Jun 2014 01:15:00 +0530 Subject: [PATCH 0075/1122] Made test Python3 compatible --- skimage/graph/rag.py | 37 ++++++++++++++++++++------------- skimage/graph/tests/test_rag.py | 8 +++---- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index dcda853b..01376734 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -5,6 +5,7 @@ from scipy import ndimage as nd class RAG(nx.Graph): + """ The class for holding the Region Adjacency Graph (RAG). @@ -13,7 +14,8 @@ class RAG(nx.Graph): between their corresponding nodes. """ - def merge_nodes(self, i, j, function=None, extra_arguments=[], extra_keywords={}): + def merge_nodes(self, i, j, function=None, extra_arguments=[], + extra_keywords={}): """Merge node `i` into `j`. The new combined node is adjacent to all the neighbors of `i` @@ -33,7 +35,7 @@ class RAG(nx.Graph): extra_arguments : sequence, optional The sequence of extra positional arguments passed to `function` - extra_keywords : + extra_keywords : The dict of keyword arguments passed to the `function`. """ for x in self.neighbors(i): @@ -43,13 +45,14 @@ class RAG(nx.Graph): w2 = -1 if self.has_edge(x, j): w2 = self.get_edge_data(x, j)['weight'] - + w = w1 - if w2 > 0 : - if not function : + if w2 > 0: + if not function: w = max(w1, w2) else: - w = function((i, x), (j,x), self, *extra_arguments, **extra_keywords) + w = function((i, x), (j, x), self, + *extra_arguments, **extra_keywords) self.add_edge(x, j, weight=w) self.node[j]['labels'] += self.node[i]['labels'] @@ -82,7 +85,7 @@ def _add_edge_filter(values, g): return 0.0 -def rag_meancolor(image, label_image, connectivity = 2): +def rag_meancolor(image, label_image, connectivity=2): """Compute the Region Adjacency Graph of a color image using difference in mean color of regions as edge weights. @@ -131,6 +134,12 @@ def rag_meancolor(image, label_image, connectivity = 2): # The footprint is constructed in such a way that the first # element in the array being passed to _add_edge_filter is # the central value. + + for i in range(label_image.max() + 1): + g.add_node( + i, {'labels': [i], 'pixel count': 0, 'total color': + np.array([0, 0, 0], dtype=np.double)}) + filters.generic_filter( label_image, function=_add_edge_filter, @@ -141,13 +150,13 @@ def rag_meancolor(image, label_image, connectivity = 2): for index in np.ndindex(label_image.shape): current = label_image[index] - if 'pixel count' in g.node[current]: - g.node[current]['pixel count'] += 1 - g.node[current]['total color'] += image[index] - else: - g.node[current]['pixel count'] = 1 - g.node[current]['total color'] = image[index].astype(np.double) - g.node[current]['labels'] = [current] + # if 'pixel count' in g.node[current]: + g.node[current]['pixel count'] += 1 + g.node[current]['total color'] += image[index] + # else: + # g.node[current]['pixel count'] = 1 + # g.node[current]['total color'] = image[index].astype(np.double) + # g.node[current]['labels'] = [current] for n in g: g.node[n]['mean color'] = (g.node[n]['total color'] / diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index c644560c..015999f5 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -2,9 +2,9 @@ import numpy as np from skimage import graph import random -def _min_edge((a1,b1),(a2,b2),g): - w1 = g.edge[a1][b1]['weight'] - w2 = g.edge[a2][b2]['weight'] +def _min_edge(e1,e2,g): + w1 = g.edge[e1[0]][e1[1]]['weight'] + w2 = g.edge[e2[0]][e2[1]]['weight'] return min(w1,w2) def test_rag_merge(): @@ -28,7 +28,7 @@ def test_rag_merge(): g.merge_nodes(x,y,_min_edge) idx = g.nodes()[0] - assert sorted(g.node[idx]['labels']) == range(10) + assert sorted(g.node[idx]['labels']) == list(range(10)) assert g.edges() == [] From c14021a9cbd16f07526adf08a7aff508521b6f0a Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 23 Jun 2014 01:16:57 +0530 Subject: [PATCH 0076/1122] Formatting and typos --- skimage/graph/graph_cut.py | 2 +- skimage/graph/tests/test_rag.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 27e4400d..1105d423 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -46,7 +46,7 @@ def threshold_cut(label_image, rag, thresh): comps = nx.connected_components(rag) - map_array = np.arange(label_image.max()+1, dtype = np.int) + map_array = np.arange(label_image.max()+1, dtype=np.int) for i, nodes in enumerate(comps): for node in nodes: for label in rag.node[node]['labels']: diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 015999f5..bdaf8277 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -2,10 +2,12 @@ import numpy as np from skimage import graph import random -def _min_edge(e1,e2,g): + +def _min_edge(e1, e2, g): w1 = g.edge[e1[0]][e1[1]]['weight'] w2 = g.edge[e2[0]][e2[1]]['weight'] - return min(w1,w2) + return min(w1, w2) + def test_rag_merge(): g = graph.rag.RAG() @@ -16,16 +18,16 @@ def test_rag_merge(): for i in range(4): x = random.choice(g.nodes()) y = random.choice(g.nodes()) - while x == y : + while x == y: y = random.choice(g.nodes()) - g.merge_nodes(x,y) - + g.merge_nodes(x, y) + for i in range(5): x = random.choice(g.nodes()) y = random.choice(g.nodes()) - while x == y : + while x == y: y = random.choice(g.nodes()) - g.merge_nodes(x,y,_min_edge) + g.merge_nodes(x, y, _min_edge) idx = g.nodes()[0] assert sorted(g.node[idx]['labels']) == list(range(10)) From 66d3ec3831b358e51f93d7599b0f98a38f3f6486 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 23 Jun 2014 01:28:10 +0530 Subject: [PATCH 0077/1122] removed commented stataments --- skimage/graph/rag.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 01376734..24c82a19 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -149,14 +149,8 @@ def rag_meancolor(image, label_image, connectivity=2): for index in np.ndindex(label_image.shape): current = label_image[index] - - # if 'pixel count' in g.node[current]: g.node[current]['pixel count'] += 1 g.node[current]['total color'] += image[index] - # else: - # g.node[current]['pixel count'] = 1 - # g.node[current]['total color'] = image[index].astype(np.double) - # g.node[current]['labels'] = [current] for n in g: g.node[n]['mean color'] = (g.node[n]['total color'] / From 27831d8e2ffd683d802572b1b71340268002f430 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 23 Jun 2014 22:26:53 +0530 Subject: [PATCH 0078/1122] Formatting and docstring changes --- skimage/graph/graph_cut.py | 4 ++-- skimage/graph/rag.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 1105d423..6cdd9f83 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -3,7 +3,7 @@ import numpy as np def threshold_cut(label_image, rag, thresh): - """Combines regions seperated by weight less than threshold. + """Combine regions seperated by weight less than threshold. Given an image's labels and its RAG, outputs new labels by combining regions whose nodes are seperated by a weight less @@ -46,7 +46,7 @@ def threshold_cut(label_image, rag, thresh): comps = nx.connected_components(rag) - map_array = np.arange(label_image.max()+1, dtype=np.int) + map_array = np.arange(label_image.max() + 1, dtype=np.int) for i, nodes in enumerate(comps): for node in nodes: for label in rag.node[node]['labels']: diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 24c82a19..673e7ead 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -96,10 +96,11 @@ def rag_meancolor(image, label_image, connectivity=2): Parameters ---------- - image : (width, height, 3) or (width, height, depth, 3) ndarray + image : ndarray Input image. - label_image : (width, height) or (width, height, depth) ndarray - The array with labels. + label_image : ndarray + The array with labels. This should have one dimention lesser than + `image` connectivity : float, optional Pixels with a squared distance less than `connectivity`from each other are considered adjacent. From bd9ab8f0fd9b4b6f6987ad9d86a1a10fe572901e Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 23 Jun 2014 23:04:16 +0530 Subject: [PATCH 0079/1122] Switch label_image to labels --- skimage/graph/graph_cut.py | 8 ++++---- skimage/graph/rag.py | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 6cdd9f83..87fa79a8 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -2,7 +2,7 @@ import networkx as nx import numpy as np -def threshold_cut(label_image, rag, thresh): +def threshold_cut(labels, rag, thresh): """Combine regions seperated by weight less than threshold. Given an image's labels and its RAG, outputs new labels by @@ -11,7 +11,7 @@ def threshold_cut(label_image, rag, thresh): Parameters ---------- - label_image : (width, height) or (width, height, 3) ndarray + labels : (width, height) or (width, height, 3) ndarray The array of labels. rag : RAG The region adjacency graph. @@ -46,10 +46,10 @@ def threshold_cut(label_image, rag, thresh): comps = nx.connected_components(rag) - map_array = np.arange(label_image.max() + 1, dtype=np.int) + map_array = np.arange(labels.max() + 1, dtype=np.int) for i, nodes in enumerate(comps): for node in nodes: for label in rag.node[node]['labels']: map_array[label] = i - return map_array[label_image] + return map_array[labels] diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 673e7ead..e42a5707 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -85,7 +85,7 @@ def _add_edge_filter(values, g): return 0.0 -def rag_meancolor(image, label_image, connectivity=2): +def rag_meancolor(image, labels, connectivity=2): """Compute the Region Adjacency Graph of a color image using difference in mean color of regions as edge weights. @@ -98,7 +98,7 @@ def rag_meancolor(image, label_image, connectivity=2): ---------- image : ndarray Input image. - label_image : ndarray + labels : ndarray The array with labels. This should have one dimention lesser than `image` connectivity : float, optional @@ -126,7 +126,7 @@ def rag_meancolor(image, label_image, connectivity=2): """ g = RAG() - fp = nd.generate_binary_structure(label_image.ndim, connectivity) + fp = nd.generate_binary_structure(labels.ndim, connectivity) for d in range(fp.ndim): fp = fp.swapaxes(0, d) fp[0, ...] = 0 @@ -136,20 +136,20 @@ def rag_meancolor(image, label_image, connectivity=2): # element in the array being passed to _add_edge_filter is # the central value. - for i in range(label_image.max() + 1): + for i in range(labels.max() + 1): g.add_node( i, {'labels': [i], 'pixel count': 0, 'total color': np.array([0, 0, 0], dtype=np.double)}) filters.generic_filter( - label_image, + labels, function=_add_edge_filter, footprint=fp, mode='nearest', extra_arguments=(g,)) - for index in np.ndindex(label_image.shape): - current = label_image[index] + for index in np.ndindex(labels.shape): + current = labels[index] g.node[current]['pixel count'] += 1 g.node[current]['total color'] += image[index] From b1f59fceee1cb2f9a11c9ff96412b771d2a63ae5 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 26 Jun 2014 00:11:49 +0530 Subject: [PATCH 0080/1122] Changed prototype of weight_func --- doc/examples/plot_rag_meancolor.py | 4 +- skimage/graph/graph_cut.py | 10 ++-- skimage/graph/rag.py | 95 +++++++++++++++--------------- skimage/graph/tests/test_rag.py | 18 ++++-- 4 files changed, 65 insertions(+), 62 deletions(-) diff --git a/doc/examples/plot_rag_meancolor.py b/doc/examples/plot_rag_meancolor.py index 1f19dd5e..129814e2 100644 --- a/doc/examples/plot_rag_meancolor.py +++ b/doc/examples/plot_rag_meancolor.py @@ -9,9 +9,7 @@ difference in mean color. We then join regions with similar mean color. """ -from skimage import graph -from skimage import segmentation -from skimage import data, io +from skimage import graph, data, io, segmentation, color from matplotlib import pyplot as plt from skimage import color diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 87fa79a8..165091b1 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -5,13 +5,13 @@ import numpy as np def threshold_cut(labels, rag, thresh): """Combine regions seperated by weight less than threshold. - Given an image's labels and its RAG, outputs new labels by + Given an image's labels and its RAG, output new labels by combining regions whose nodes are seperated by a weight less than the given threshold. Parameters ---------- - labels : (width, height) or (width, height, 3) ndarray + labels : ndarray The array of labels. rag : RAG The region adjacency graph. @@ -21,12 +21,12 @@ def threshold_cut(labels, rag, thresh): Returns ------- - out : (width, height, 3) or (width, height, depth, 3) ndarray + out : ndarray The new labelled array. Examples -------- - >>> from skimage import data,graph,segmentation + >>> from skimage import data, graph, segmentation >>> img = data.lena() >>> labels = segmentation.slic(img) >>> rag = graph.rag_meancolor(img, labels) @@ -46,7 +46,7 @@ def threshold_cut(labels, rag, thresh): comps = nx.connected_components(rag) - map_array = np.arange(labels.max() + 1, dtype=np.int) + map_array = np.arange(labels.max() + 1, dtype=labels.dtype) for i, nodes in enumerate(comps): for node in nodes: for label in rag.node[node]['labels']: diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index e42a5707..1012ccb5 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -7,56 +7,50 @@ from scipy import ndimage as nd class RAG(nx.Graph): """ - The class for holding the Region Adjacency Graph (RAG). - - Each region is a contiguous set of pixels in an image, usually - sharing some common property. Adjacent regions have an edge - between their corresponding nodes. + The Region Adjacency Graph (RAG) of an image. """ - def merge_nodes(self, i, j, function=None, extra_arguments=[], + def merge_nodes(self, src, dst, weight_func=None, extra_arguments=[], extra_keywords={}): - """Merge node `i` into `j`. + """Merge two nodes. - The new combined node is adjacent to all the neighbors of `i` - and `j`. In case of conflicting edges the given function is + The new combined node is adjacent to all the neighbors of `src` + and `dst`. In case of conflicting edges the given function is called. Parameters ---------- i, j : int Nodes to be merged. The resulting node will have ID `j`. - function : callable, optional - Function to decide which edge weight to keep when a node is - adjacent to both `i` and `j`. The arguments passed to the - function are, the tuples represnting both the conflicting edges - and the graph.The default behaviour is that the edge with higher - weight is kept. + weight_func : callable, optional + Function to decide edge weight between existing nodes and the new + node.The arguments passed to the function are, the graph, `src`, + `dst` and the existing node whose edge weight need to be updated. extra_arguments : sequence, optional The sequence of extra positional arguments passed to - `function` + `weight_func` extra_keywords : - The dict of keyword arguments passed to the `function`. + The dict of keyword arguments passed to the `weight_func`. """ - for x in self.neighbors(i): - if x == j: + for neighbor in self.neighbors(src): + if neighbor == dst: continue - w1 = self.get_edge_data(x, i)['weight'] - w2 = -1 - if self.has_edge(x, j): - w2 = self.get_edge_data(x, j)['weight'] - - w = w1 - if w2 > 0: - if not function: - w = max(w1, w2) + w1 = self.get_edge_data(neighbor, src)['weight'] + w2 = None + if self.has_edge(neighbor, dst): + w2 = self.get_edge_data(neighbor, dst)['weight'] + if not weight_func: + if w2 is None: + w = w1 else: - w = function((i, x), (j, x), self, - *extra_arguments, **extra_keywords) - self.add_edge(x, j, weight=w) + w = min(w1, w2) + else: + w = weight_func(self, src, dst, neighbor, + *extra_arguments, **extra_keywords) + self.add_edge(neighbor, dst, weight=w) - self.node[j]['labels'] += self.node[i]['labels'] - self.remove_node(i) + self.node[dst]['labels'] += self.node[src]['labels'] + self.remove_node(src) def _add_edge_filter(values, g): @@ -86,24 +80,27 @@ def _add_edge_filter(values, g): def rag_meancolor(image, labels, connectivity=2): - """Compute the Region Adjacency Graph of a color image using - difference in mean color of regions as edge weights. + """Compute the Region Adjacency Graph using mean colors. Given an image and its segmentation, this method constructs the corresponsing Region Adjacency Graph (RAG). Each node in the RAG represents a contiguous pixels with in `img` the same label in - `arr`. + `arr`. The weight between two adjacent regions is the difference + int their mean color. Parameters ---------- image : ndarray Input image. labels : ndarray - The array with labels. This should have one dimention lesser than - `image` + The array with labels. This should have one dimention less than + `image`. If `image` has dimensions `(M,N,3)` `labels` should have + dimensions `(M, N)`. connectivity : float, optional - Pixels with a squared distance less than `connectivity`from each other - are considered adjacent. + Pixels with a squared distance less than `connectivity` from each other + are considered adjacent. It can range from 1 to `labels.ndim`. It's + behaviour is the same as `connectivity` parameter in + `scipy.ndimage.filters.generate_binary_structure`. Returns ------- @@ -126,28 +123,28 @@ def rag_meancolor(image, labels, connectivity=2): """ g = RAG() + # The footprint is constructed in such a way that the first + # element in the array being passed to _add_edge_filter is + # the central value. fp = nd.generate_binary_structure(labels.ndim, connectivity) for d in range(fp.ndim): fp = fp.swapaxes(0, d) fp[0, ...] = 0 fp = fp.swapaxes(0, d) - # The footprint is constructed in such a way that the first - # element in the array being passed to _add_edge_filter is - # the central value. - - for i in range(labels.max() + 1): - g.add_node( - i, {'labels': [i], 'pixel count': 0, 'total color': - np.array([0, 0, 0], dtype=np.double)}) - filters.generic_filter( labels, function=_add_edge_filter, footprint=fp, mode='nearest', + output=np.zeros(labels.shape, dtype=np.uint8), extra_arguments=(g,)) + for n in g: + g.node[n].update({'labels': [n], + 'pixel count': 0, + 'total color': np.array([0, 0, 0], dtype=np.double)}) + for index in np.ndindex(labels.shape): current = labels[index] g.node[current]['pixel count'] += 1 diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index bdaf8277..5d382a43 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -3,10 +3,18 @@ from skimage import graph import random -def _min_edge(e1, e2, g): - w1 = g.edge[e1[0]][e1[1]]['weight'] - w2 = g.edge[e2[0]][e2[1]]['weight'] - return min(w1, w2) +def _max_edge(g, src, dst, neighbor): + try: + w1 = g.edge[src][neighbor]['weight'] + except KeyError: + w1 = None + + try: + w2 = g.edge[dst][neighbor]['weight'] + except KeyError: + w2 = None + + return max(w1, w2) def test_rag_merge(): @@ -27,7 +35,7 @@ def test_rag_merge(): y = random.choice(g.nodes()) while x == y: y = random.choice(g.nodes()) - g.merge_nodes(x, y, _min_edge) + g.merge_nodes(x, y, _max_edge) idx = g.nodes()[0] assert sorted(g.node[idx]['labels']) == list(range(10)) From cbfa8aa6e12e03947c757ca644e76749d3a6fcf5 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 26 Jun 2014 00:54:16 +0530 Subject: [PATCH 0081/1122] docstring corrections --- skimage/graph/rag.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 1012ccb5..8017f1f6 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -15,22 +15,23 @@ class RAG(nx.Graph): """Merge two nodes. The new combined node is adjacent to all the neighbors of `src` - and `dst`. In case of conflicting edges the given function is - called. + and `dst`. `weight_func` is called to decide the weight of edges + incident on the new node. Parameters ---------- i, j : int - Nodes to be merged. The resulting node will have ID `j`. + Nodes to be merged. The resulting node will have ID `dst`. weight_func : callable, optional - Function to decide edge weight between existing nodes and the new - node.The arguments passed to the function are, the graph, `src`, - `dst` and the existing node whose edge weight need to be updated. + Function to decide edge weight of edges incident on the new node. + The arguments passed to the function are, the graph, `src`, `dst` + and the node which is adjacent to the new node. extra_arguments : sequence, optional The sequence of extra positional arguments passed to - `weight_func` + `weight_func`. extra_keywords : The dict of keyword arguments passed to the `weight_func`. + """ for neighbor in self.neighbors(src): if neighbor == dst: @@ -54,8 +55,11 @@ class RAG(nx.Graph): def _add_edge_filter(values, g): - """Add an edge between first element in `values` and - all other elements of `values` in the graph `g`.`values[0]` + """Create and edge between the first and the remaining + values in an array. + + Add an edge between first element in `values` and + all other elements of `values` in the graph `g`. `values[0]` is expected to be the central value of the footprint used. Parameters @@ -67,7 +71,7 @@ def _add_edge_filter(values, g): Returns ------- - 0.0 : float + 0 : int Always returns 0. """ @@ -76,7 +80,7 @@ def _add_edge_filter(values, g): for value in values[1:]: g.add_edge(current, value) - return 0.0 + return 0 def rag_meancolor(image, labels, connectivity=2): @@ -84,9 +88,9 @@ def rag_meancolor(image, labels, connectivity=2): Given an image and its segmentation, this method constructs the corresponsing Region Adjacency Graph (RAG). Each node in the RAG - represents a contiguous pixels with in `img` the same label in - `arr`. The weight between two adjacent regions is the difference - int their mean color. + represents a contiguous set pixels within `image` with the same + label in `labels`. The weight between two adjacent regions is the + difference int their mean color. Parameters ---------- @@ -94,7 +98,7 @@ def rag_meancolor(image, labels, connectivity=2): Input image. labels : ndarray The array with labels. This should have one dimention less than - `image`. If `image` has dimensions `(M,N,3)` `labels` should have + `image`. If `image` has dimensions `(M, N, 3)` `labels` should have dimensions `(M, N)`. connectivity : float, optional Pixels with a squared distance less than `connectivity` from each other @@ -109,7 +113,7 @@ def rag_meancolor(image, labels, connectivity=2): Examples -------- - >>> from skimage import data,graph,segmentation + >>> from skimage import data, graph, segmentation >>> img = data.lena() >>> labels = segmentation.slic(img) >>> rag = graph.rag_meancolor(img, labels) From b1284ee18015432a475128ff06195bce54539639 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 26 Jun 2014 00:57:50 +0530 Subject: [PATCH 0082/1122] update test for py3 --- skimage/graph/tests/test_rag.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 5d382a43..71d44774 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -14,7 +14,12 @@ def _max_edge(g, src, dst, neighbor): except KeyError: w2 = None - return max(w1, w2) + if w1 == None: + return w2 + elif w2 == None: + return w1 + else: + return max(w1, w2) def test_rag_merge(): From bb874074c4d30b88d01d14e93ca96e2e69d7de7b Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 26 Jun 2014 00:58:54 +0530 Subject: [PATCH 0083/1122] None comparison --- skimage/graph/tests/test_rag.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 71d44774..9f7c3ba8 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -14,9 +14,9 @@ def _max_edge(g, src, dst, neighbor): except KeyError: w2 = None - if w1 == None: + if w1 is None: return w2 - elif w2 == None: + elif w2 is None: return w1 else: return max(w1, w2) From f481724f35a24d0dfeac9f1893468dd14e45ec78 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 26 Jun 2014 02:02:33 +0530 Subject: [PATCH 0084/1122] Added RAG merge example --- doc/examples/plot_rag.py | 69 ++++++++++++++++++++++++++++++ doc/examples/plot_rag_meancolor.py | 1 - 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 doc/examples/plot_rag.py diff --git a/doc/examples/plot_rag.py b/doc/examples/plot_rag.py new file mode 100644 index 00000000..d0ed3089 --- /dev/null +++ b/doc/examples/plot_rag.py @@ -0,0 +1,69 @@ +""" +======================= +Region Adjacency Graphs +======================= + +This example demonstrates the use of `merge_nodes` function of a Region +Adjacency Graph (RAG). When a new node is formed by merging two nodes, the edge +weight of all the edges incident on it can be updated by a user defined +function `weight_func`. + +The default behaviour is to use the smaller edge weight incase of a conflict. +THe example below also shows how to use a custom function to take the larger +weight instead. + +""" +from skimage.graph import rag +import networkx as nx +from matplotlib import pyplot as plt + + +def max_edge(g, src, dst, neighbor): + try: + w1 = g.edge[src][neighbor]['weight'] + except KeyError: + w1 = None + + try: + w2 = g.edge[dst][neighbor]['weight'] + except KeyError: + w2 = None + + if w1 is None: + return w2 + elif w2 is None: + return w1 + else: + return max(w1, w2) + + +def display(g, title): + pos = nx.circular_layout(g) + plt.figure() + plt.title(title) + nx.draw(g, pos) + nx.draw_networkx_edge_labels(g, pos, font_size=20) + + +g = rag.RAG() +g.add_edge(1, 2, weight=10) +g.add_edge(2, 3, weight=20) +g.add_edge(3, 4, weight=30) +g.add_edge(4, 1, weight=40) +g.add_edge(1, 3, weight=50) + +# Assigning dummy labels. +for n in g.nodes(): + g.node[n]['labels'] = [n] + +gc = g.copy() + +display(g, "Original Graph") + +g.merge_nodes(1, 3) +display(g, "Merged with default (min)") + +gc.merge_nodes(1, 3, weight_func=max_edge) +display(gc, "Merged with max") + +plt.show() diff --git a/doc/examples/plot_rag_meancolor.py b/doc/examples/plot_rag_meancolor.py index 129814e2..11aca77c 100644 --- a/doc/examples/plot_rag_meancolor.py +++ b/doc/examples/plot_rag_meancolor.py @@ -11,7 +11,6 @@ difference in mean color. We then join regions with similar mean color. from skimage import graph, data, io, segmentation, color from matplotlib import pyplot as plt -from skimage import color img = data.coffee() From c08a6876d647448d520ad8893065a8baac4be46c Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 26 Jun 2014 02:05:20 +0530 Subject: [PATCH 0085/1122] Added comment about footprint --- skimage/graph/rag.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 8017f1f6..4943cf4a 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -136,6 +136,13 @@ def rag_meancolor(image, labels, connectivity=2): fp[0, ...] = 0 fp = fp.swapaxes(0, d) + # For example + # if labels.ndim = 2 and connectivity = 1 + # fp = [[0,0,0],[0,1,1],[0,1,0]] + # + # if labels.ndim = 2 and connectivity = 2 + # fp = [[0,0,0],[0,1,1],[0,1,1]] + filters.generic_filter( labels, function=_add_edge_filter, From b9a7445296a650c7639efe1f71c737ccfa1630e6 Mon Sep 17 00:00:00 2001 From: Kevin Murray Date: Fri, 27 Jun 2014 21:26:58 +1000 Subject: [PATCH 0086/1122] [skimg.io._plugins.freeimage_plugin] fix segfault I have moved the freeimage error handler callback function to the module namespace to prevent it being garbage collected. See the following for more info on this quirk of ctypes: http://stackoverflow.com/questions/12995925/how-to-prevent-functype-from-being-collected https://github.com/JohannesBuchner/PyMultiNest/issues/5 This also changes the way FreeImage errors are handled. If an exception is raised in a callback, it will not propagate beyond ctypes internals. Now, we use a callback that sets a global variable to indicate error. We then check for error and reset the error string to NULL every time the C api is called. This is the only way we can both: a) Not segfault on freeimage error b) Pass the freeimage error to the user c) raise RuntimeError() --- skimage/io/_plugins/freeimage_plugin.py | 62 +++++++++++++++++++------ 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/skimage/io/_plugins/freeimage_plugin.py b/skimage/io/_plugins/freeimage_plugin.py index a26125fa..07e8d313 100644 --- a/skimage/io/_plugins/freeimage_plugin.py +++ b/skimage/io/_plugins/freeimage_plugin.py @@ -34,15 +34,29 @@ def _generate_candidate_libs(): return lib_dirs, lib_paths +if sys.platform == 'win32': + LOADER = ctypes.windll + FUNCTYPE = ctypes.WINFUNCTYPE +else: + LOADER = ctypes.cdll + FUNCTYPE = ctypes.CFUNCTYPE + +def handle_errors(): + global FT_ERROR_STR + if FT_ERROR_STR: + tmp = FT_ERROR_STR + FT_ERROR_STR = None + raise RuntimeError(tmp) + +FT_ERROR_STR = None +# This MUST happen in module scope, or the function pointer is garbage +# collected, leading to a segfault when error_handler is called. +@FUNCTYPE(None, ctypes.c_int, ctypes.c_char_p) +def c_error_handler(fif, message): + global FT_ERROR_STR + FT_ERROR_STR = 'FreeImage error: %s' % message def load_freeimage(): - if sys.platform == 'win32': - loader = ctypes.windll - functype = ctypes.WINFUNCTYPE - else: - loader = ctypes.cdll - functype = ctypes.CFUNCTYPE - freeimage = None errors = [] # First try a few bare library names that ctypes might be able to find @@ -54,7 +68,7 @@ def load_freeimage(): lib_paths = bare_libs + lib_paths for lib in lib_paths: try: - freeimage = loader.LoadLibrary(lib) + freeimage = LOADER.LoadLibrary(lib) break except Exception: if lib not in bare_libs: @@ -81,11 +95,7 @@ def load_freeimage(): '\n'.join(lib_dirs)) # FreeImage found - @functype(None, ctypes.c_int, ctypes.c_char_p) - def error_handler(fif, message): - raise RuntimeError('FreeImage error: %s' % message) - - freeimage.FreeImage_SetOutputMessage(error_handler) + freeimage.FreeImage_SetOutputMessage(c_error_handler) return freeimage _FI = load_freeimage() @@ -189,13 +199,17 @@ class FI_TYPES(object): @classmethod def get_type_and_shape(cls, bitmap): w = _FI.FreeImage_GetWidth(bitmap) + handle_errors() h = _FI.FreeImage_GetHeight(bitmap) + handle_errors() fi_type = _FI.FreeImage_GetImageType(bitmap) + handle_errors() if not fi_type: raise ValueError('Unknown image pixel type') dtype = cls.dtypes[fi_type] if fi_type == cls.FIT_BITMAP: bpp = _FI.FreeImage_GetBPP(bitmap) + handle_errors() if bpp == 8: extra_dims = [] elif bpp == 24: @@ -377,9 +391,11 @@ class METADATA_DATATYPE(object): def _process_bitmap(filename, flags, process_func): filename = asbytes(filename) ftype = _FI.FreeImage_GetFileType(filename, 0) + handle_errors() if ftype == -1: raise ValueError('Cannot determine type of file %s' % filename) bitmap = _FI.FreeImage_Load(ftype, filename, flags) + handle_errors() bitmap = ctypes.c_void_p(bitmap) if not bitmap: raise ValueError('Could not load file %s' % filename) @@ -387,6 +403,7 @@ def _process_bitmap(filename, flags, process_func): return process_func(bitmap) finally: _FI.FreeImage_Unload(bitmap) + handle_errors() def read(filename, flags=0): @@ -414,6 +431,7 @@ def read_metadata(filename): def _process_multipage(filename, flags, process_func): filename = asbytes(filename) ftype = _FI.FreeImage_GetFileType(filename, 0) + handle_errors() if ftype == -1: raise ValueError('Cannot determine type of file %s' % filename) create_new = False @@ -422,14 +440,17 @@ def _process_multipage(filename, flags, process_func): multibitmap = _FI.FreeImage_OpenMultiBitmap(ftype, filename, create_new, read_only, keep_cache_in_memory, flags) + handle_errors() multibitmap = ctypes.c_void_p(multibitmap) if not multibitmap: raise ValueError('Could not open %s as multi-page image.' % filename) try: pages = _FI.FreeImage_GetPageCount(multibitmap) + handle_errors() out = [] for i in range(pages): bitmap = _FI.FreeImage_LockPage(multibitmap, i) + handle_errors() bitmap = ctypes.c_void_p(bitmap) if not bitmap: raise ValueError('Could not open %s as a multi-page image.' @@ -438,9 +459,11 @@ def _process_multipage(filename, flags, process_func): out.append(process_func(bitmap)) finally: _FI.FreeImage_UnlockPage(multibitmap, bitmap, False) + handle_errors() return out finally: _FI.FreeImage_CloseMultiBitmap(multibitmap, 0) + handle_errors() def read_multipage(filename, flags=0): @@ -469,6 +492,7 @@ def _wrap_bitmap_bits_in_array(bitmap, shape, dtype): """ pitch = _FI.FreeImage_GetPitch(bitmap) + handle_errors() height = shape[-1] byte_size = height * pitch itemsize = dtype.itemsize @@ -478,6 +502,7 @@ def _wrap_bitmap_bits_in_array(bitmap, shape, dtype): else: strides = (itemsize, pitch) bits = _FI.FreeImage_GetBits(bitmap) + handle_errors() array = numpy.ndarray(shape, dtype=dtype, buffer=(ctypes.c_char * byte_size).from_address(bits), strides=strides) @@ -502,6 +527,7 @@ def _array_from_bitmap(bitmap): g = n(array[1]) r = n(array[2]) if shape[0] == 3: + handle_errors() return numpy.dstack((r, g, b)) elif shape[0] == 4: a = n(array[3]) @@ -523,6 +549,7 @@ def _read_metadata(bitmap): for model_name, number in models: mdhandle = _FI.FreeImage_FindFirstMetadata(number, bitmap, ctypes.byref(tag)) + handle_errors() mdhandle = ctypes.c_void_p(mdhandle) if mdhandle: more = True @@ -530,8 +557,10 @@ def _read_metadata(bitmap): tag_name = asstr(_FI.FreeImage_GetTagKey(tag)) tag_type = _FI.FreeImage_GetTagType(tag) byte_size = _FI.FreeImage_GetTagLength(tag) + handle_errors() char_ptr = ctypes.c_char * byte_size tag_str = char_ptr.from_address(_FI.FreeImage_GetTagValue(tag)) + handle_errors() if tag_type == METADATA_DATATYPE.FIDT_ASCII: tag_val = asstr(tag_str.value) else: @@ -541,7 +570,9 @@ def _read_metadata(bitmap): tag_val = tag_val[0] metadata[(model_name, tag_name)] = tag_val more = _FI.FreeImage_FindNextMetadata(mdhandle, ctypes.byref(tag)) + handle_errors() _FI.FreeImage_FindCloseMetadata(mdhandle) + handle_errors() return metadata @@ -556,6 +587,7 @@ def write(array, filename, flags=0): array = numpy.asarray(array) filename = asbytes(filename) ftype = _FI.FreeImage_GetFIFFromFilename(filename) + handle_errors() if ftype == -1: raise ValueError('Cannot determine type for %s' % filename) bitmap, fi_type = _array_to_bitmap(array) @@ -563,16 +595,20 @@ def write(array, filename, flags=0): if fi_type == FI_TYPES.FIT_BITMAP: can_write = _FI.FreeImage_FIFSupportsExportBPP(ftype, _FI.FreeImage_GetBPP(bitmap)) + handle_errors() else: can_write = _FI.FreeImage_FIFSupportsExportType(ftype, fi_type) + handle_errors() if not can_write: raise TypeError('Cannot save image of this format ' 'to this file type') res = _FI.FreeImage_Save(ftype, bitmap, filename, flags) + handle_errors() if not res: raise RuntimeError('Could not save image properly.') finally: _FI.FreeImage_Unload(bitmap) + handle_errors() def write_multipage(arrays, filename, flags=0): From c8480d3f6d4a6fd990f9b35975216615d1b18f9d Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 28 Jun 2014 01:17:44 +0530 Subject: [PATCH 0087/1122] Change merge_nodes code and test case --- skimage/graph/rag.py | 101 +++++++++++++++++++++----------- skimage/graph/tests/test_rag.py | 61 ++++++++----------- 2 files changed, 93 insertions(+), 69 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 4943cf4a..be138e1e 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -4,13 +4,42 @@ from scipy.ndimage import filters from scipy import ndimage as nd +def min_weight(g, src, dst, n): + """Callback to handle merging nodes by choosing minimum weight. + + Returns either the weight between (`src`, `n`) or (`dst`, `n`) + in `g` or the minumum of the two when both exist. + + Parameters + ---------- + g : RAG + The graph to consider. + src, dst : int + Typically the verices in `g` to be merged. + n : int + A neighbor of `src` or `dst` or both + + Returns + ------- + weight : float + The weight between (`src`, `n`) or (`dst`, `n`) in `g` or the + minumum of the two when both exist. + + """ + + # cover the cases where n only has edge to either `src` or `dst` + w1 = g[n].get(src, {'weight': np.inf})['weight'] + w2 = g[n].get(dst, {'weight': np.inf})['weight'] + return min(w1, w2) + + class RAG(nx.Graph): """ The Region Adjacency Graph (RAG) of an image. """ - def merge_nodes(self, src, dst, weight_func=None, extra_arguments=[], + def merge_nodes(self, src, dst, weight_func=min_weight, extra_arguments=[], extra_keywords={}): """Merge two nodes. @@ -20,12 +49,13 @@ class RAG(nx.Graph): Parameters ---------- - i, j : int + src, dst : int Nodes to be merged. The resulting node will have ID `dst`. weight_func : callable, optional Function to decide edge weight of edges incident on the new node. - The arguments passed to the function are, the graph, `src`, `dst` - and the node which is adjacent to the new node. + For each neighbor `n` for `src and `dst`, `weight_func` will be + called as follows: `weight_func(src, dst, n, *extra_arguments, + **extra_keywords)` extra_arguments : sequence, optional The sequence of extra positional arguments passed to `weight_func`. @@ -33,21 +63,21 @@ class RAG(nx.Graph): The dict of keyword arguments passed to the `weight_func`. """ - for neighbor in self.neighbors(src): - if neighbor == dst: - continue - w1 = self.get_edge_data(neighbor, src)['weight'] - w2 = None - if self.has_edge(neighbor, dst): - w2 = self.get_edge_data(neighbor, dst)['weight'] - if not weight_func: - if w2 is None: - w = w1 - else: - w = min(w1, w2) - else: - w = weight_func(self, src, dst, neighbor, - *extra_arguments, **extra_keywords) + neighbors = self.adj[src].copy() + neighbors.update(self.adj[dst]) + + try: + del neighbors[src] + except KeyError: + pass + try: + del neighbors[dst] + except KeyError: + pass + + for neighbor in neighbors: + w = weight_func(self, src, dst, neighbor, *extra_arguments, + **extra_keywords) self.add_edge(neighbor, dst, weight=w) self.node[dst]['labels'] += self.node[src]['labels'] @@ -55,8 +85,7 @@ class RAG(nx.Graph): def _add_edge_filter(values, g): - """Create and edge between the first and the remaining - values in an array. + """Create edge in `g` between first element of `values` and the rest. Add an edge between first element in `values` and all other elements of `values` in the graph `g`. `values[0]` @@ -86,24 +115,24 @@ def _add_edge_filter(values, g): def rag_meancolor(image, labels, connectivity=2): """Compute the Region Adjacency Graph using mean colors. - Given an image and its segmentation, this method constructs the + Given an image and its initial segmentation, this method constructs the corresponsing Region Adjacency Graph (RAG). Each node in the RAG - represents a contiguous set pixels within `image` with the same - label in `labels`. The weight between two adjacent regions is the - difference int their mean color. + represents a set pixels within `image` with the same + label in `labels`. The weight between two adjacent regions is the + difference in their mean color. Parameters ---------- - image : ndarray + image : ndarray, shape(M, N, [..., P,] 3) Input image. - labels : ndarray - The array with labels. This should have one dimention less than + labels : ndarray, shape(M, N, [..., P,]) + The labelled image. This should have one dimension less than `image`. If `image` has dimensions `(M, N, 3)` `labels` should have dimensions `(M, N)`. - connectivity : float, optional + connectivity : int, optional Pixels with a squared distance less than `connectivity` from each other - are considered adjacent. It can range from 1 to `labels.ndim`. It's - behaviour is the same as `connectivity` parameter in + are considered adjacent. It can range from 1 to `labels.ndim`. Its + behavior is the same as `connectivity` parameter in `scipy.ndimage.filters.generate_binary_structure`. Returns @@ -136,12 +165,16 @@ def rag_meancolor(image, labels, connectivity=2): fp[0, ...] = 0 fp = fp.swapaxes(0, d) - # For example + # For example # if labels.ndim = 2 and connectivity = 1 - # fp = [[0,0,0],[0,1,1],[0,1,0]] + # fp = [[0,0,0], + # [0,1,1], + # [0,1,0]] # # if labels.ndim = 2 and connectivity = 2 - # fp = [[0,0,0],[0,1,1],[0,1,1]] + # fp = [[0,0,0], + # [0,1,1], + # [0,1,1]] filters.generic_filter( labels, diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 9f7c3ba8..4e58ff3a 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -3,49 +3,40 @@ from skimage import graph import random -def _max_edge(g, src, dst, neighbor): - try: - w1 = g.edge[src][neighbor]['weight'] - except KeyError: - w1 = None - - try: - w2 = g.edge[dst][neighbor]['weight'] - except KeyError: - w2 = None - - if w1 is None: - return w2 - elif w2 is None: - return w1 - else: - return max(w1, w2) +def max_edge(g, src, dst, n): + w1 = g[n].get(src, {'weight': -np.inf})['weight'] + w2 = g[n].get(dst, {'weight': -np.inf})['weight'] + return max(w1, w2) def test_rag_merge(): g = graph.rag.RAG() - for i in range(10): - g.add_edge(i, (i + 1) % 10, {'weight': i * 10}) - g.node[i]['labels'] = [i] - - for i in range(4): - x = random.choice(g.nodes()) - y = random.choice(g.nodes()) - while x == y: - y = random.choice(g.nodes()) - g.merge_nodes(x, y) for i in range(5): - x = random.choice(g.nodes()) - y = random.choice(g.nodes()) - while x == y: - y = random.choice(g.nodes()) - g.merge_nodes(x, y, _max_edge) + g.add_node(i, {'labels': [i]}) - idx = g.nodes()[0] - assert sorted(g.node[idx]['labels']) == list(range(10)) - assert g.edges() == [] + g.add_edge(0, 1, {'weight': 10}) + g.add_edge(1, 2, {'weight': 20}) + g.add_edge(2, 3, {'weight': 30}) + g.add_edge(3, 0, {'weight': 40}) + g.add_edge(0, 2, {'weight': 50}) + g.add_edge(3, 4, {'weight': 60}) + gc = g.copy() + + g.merge_nodes(0, 2) + assert g.edge[1][2]['weight'] == 10 + assert g.edge[2][3]['weight'] == 30 + + gc.merge_nodes(0, 2, weight_func=max_edge) + assert gc.edge[1][2]['weight'] == 20 + assert gc.edge[2][3]['weight'] == 40 + + g.merge_nodes(1, 4) + g.merge_nodes(2, 3) + g.merge_nodes(3, 4) + assert sorted(g.node[4]['labels']) == range(5) + assert g.edges() == [] def test_threshold_cut(): From 1982a3246c9af702eafbcd1bdb43e84908fe8f6c Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 28 Jun 2014 01:32:55 +0530 Subject: [PATCH 0088/1122] Docstring changes. --- skimage/graph/rag.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index be138e1e..c7b5c66d 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -13,9 +13,9 @@ def min_weight(g, src, dst, n): Parameters ---------- g : RAG - The graph to consider. + The graph under consideration. src, dst : int - Typically the verices in `g` to be merged. + The verices in `g` to be merged. n : int A neighbor of `src` or `dst` or both @@ -59,7 +59,7 @@ class RAG(nx.Graph): extra_arguments : sequence, optional The sequence of extra positional arguments passed to `weight_func`. - extra_keywords : + extra_keywords : dictionary, optional The dict of keyword arguments passed to the `weight_func`. """ @@ -85,9 +85,9 @@ class RAG(nx.Graph): def _add_edge_filter(values, g): - """Create edge in `g` between first element of `values` and the rest. + """Create edge in `g` between the first element of `values` and the rest. - Add an edge between first element in `values` and + Add an edge between the first element in `values` and all other elements of `values` in the graph `g`. `values[0]` is expected to be the central value of the footprint used. From e63ae9450ac7b236e5dcbf859d8fc7f8741bf304 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 28 Jun 2014 01:35:41 +0530 Subject: [PATCH 0089/1122] merge_nodes docstring change --- skimage/graph/rag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index c7b5c66d..9150bfc4 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -41,7 +41,7 @@ class RAG(nx.Graph): def merge_nodes(self, src, dst, weight_func=min_weight, extra_arguments=[], extra_keywords={}): - """Merge two nodes. + """Merge node `src` into `dst`. The new combined node is adjacent to all the neighbors of `src` and `dst`. `weight_func` is called to decide the weight of edges From 87465d91d71bb5ff8db7c010844d8fe3f8800656 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 28 Jun 2014 01:39:28 +0530 Subject: [PATCH 0090/1122] test_rag.py formatting --- skimage/graph/tests/test_rag.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 4e58ff3a..af325215 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -1,6 +1,5 @@ import numpy as np from skimage import graph -import random def max_edge(g, src, dst, n): @@ -36,7 +35,8 @@ def test_rag_merge(): g.merge_nodes(2, 3) g.merge_nodes(3, 4) assert sorted(g.node[4]['labels']) == range(5) - assert g.edges() == [] + assert g.edges() == [] + def test_threshold_cut(): From 992a12f968f04f3fd4c0d90f5e081baf5996d689 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 27 Jun 2014 23:37:28 +0200 Subject: [PATCH 0091/1122] Only import numpy after dependency check --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c13ac28c..e8df6dad 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,6 @@ import os import sys import re import setuptools -from numpy.distutils.core import setup from distutils.command.build_py import build_py @@ -111,6 +110,7 @@ if __name__ == "__main__": write_version_py() + from numpy.distutils.core import setup setup( name=DISTNAME, description=DESCRIPTION, From 79d698ee112eb305e0d8843d128a4489f62246ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Fri, 27 Jun 2014 21:40:37 -0400 Subject: [PATCH 0092/1122] add user guide parallelization --- doc/source/user_guide/parallelization.txt | 60 +++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100755 doc/source/user_guide/parallelization.txt diff --git a/doc/source/user_guide/parallelization.txt b/doc/source/user_guide/parallelization.txt new file mode 100755 index 00000000..d4ffe59b --- /dev/null +++ b/doc/source/user_guide/parallelization.txt @@ -0,0 +1,60 @@ +========================= +How to parallelize loops? +========================= + +In image processing, we frequently apply the same algorithm +on a large batch of images. Let us define an example. + +.. code-block:: python + + from skimage import data, color + from skimage.restoration import denoise_tv_chambolle + from skimage.feature import hog + + def tasks(image): + """ + Apply some functions. + """ + image = denoise_tv_chambolle(image, weight=0.1, multichannel=True) + fd, hog_image = hog(color.rgb2gray(image), orientations=8, + pixels_per_cell=(16, 16), cells_per_block=(1, 1), + visualise=True) + + + # Prepare images + hubble = data.hubble_deep_field() + width = 10 + pics = [hubble[:,slice:slice+width] for slice in range(0, 1000, width)] + +To call the function ``tasks`` on each element of the list ``pics``, it is +usual to write a for loop. To measure the execution time of this loop, a function +is defined and called with ``timeit``. + +.. code-block:: python + + def classic_loop(): + for image in pics: + tasks(image) + + import timeit + print("classic_loop():", timeit.timeit("classic_loop()", setup="from __main__ import (classic_loop, tasks, pics)", number=1)) + +Another equivalent way to code this loop is to use a comprehension list which has the same efficiency. + +.. code-block:: python + + def comprehension_loop(): + [tasks(image) for image in pics] + + print("comprehension_loop():", timeit.timeit("comprehension_loop()", setup="from __main__ import (comprehension_loop, tasks, pics)", number=1)) + +``joblib`` is a library providing an easy way to parallelize for loops once we have a comprehension list. +The number of jobs can be specified. + +.. code-block:: python + + from joblib import Parallel, delayed + def joblib_loop(): + Parallel(n_jobs=4)(delayed(tasks)(i) for i in pics) + + print("joblib_loop():", timeit.timeit("joblib_loop()", setup="from __main__ import (joblib_loop, tasks, pics)", number=1)) From f71ad51f57d262c973771c5709fe277af28dc8ed Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 28 Jun 2014 10:35:14 +0530 Subject: [PATCH 0093/1122] Got Py3 test to pass --- skimage/graph/tests/test_rag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index af325215..8179b92f 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -34,7 +34,7 @@ def test_rag_merge(): g.merge_nodes(1, 4) g.merge_nodes(2, 3) g.merge_nodes(3, 4) - assert sorted(g.node[4]['labels']) == range(5) + assert sorted(g.node[4]['labels']) == list(range(5)) assert g.edges() == [] From dd67b3fce7eacdc8f97cb7dfa9c7fd51a2b00214 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 28 Jun 2014 11:06:30 +0530 Subject: [PATCH 0094/1122] Changed max_edge in example/plot_rag.py --- doc/examples/plot_rag.py | 24 ++++++------------------ skimage/graph/rag.py | 2 +- skimage/graph/tests/test_rag.py | 4 ++++ 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/doc/examples/plot_rag.py b/doc/examples/plot_rag.py index d0ed3089..203f9454 100644 --- a/doc/examples/plot_rag.py +++ b/doc/examples/plot_rag.py @@ -9,32 +9,20 @@ weight of all the edges incident on it can be updated by a user defined function `weight_func`. The default behaviour is to use the smaller edge weight incase of a conflict. -THe example below also shows how to use a custom function to take the larger +The example below also shows how to use a custom function to take the larger weight instead. """ from skimage.graph import rag import networkx as nx from matplotlib import pyplot as plt +import numpy as np -def max_edge(g, src, dst, neighbor): - try: - w1 = g.edge[src][neighbor]['weight'] - except KeyError: - w1 = None - - try: - w2 = g.edge[dst][neighbor]['weight'] - except KeyError: - w2 = None - - if w1 is None: - return w2 - elif w2 is None: - return w1 - else: - return max(w1, w2) +def max_edge(g, src, dst, n): + w1 = g[n].get(src, {'weight': -np.inf})['weight'] + w2 = g[n].get(dst, {'weight': -np.inf})['weight'] + return max(w1, w2) def display(g, title): diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 9150bfc4..69eed104 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -17,7 +17,7 @@ def min_weight(g, src, dst, n): src, dst : int The verices in `g` to be merged. n : int - A neighbor of `src` or `dst` or both + A neighbor of `src` or `dst` or both. Returns ------- diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 8179b92f..37c31887 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -23,10 +23,14 @@ def test_rag_merge(): gc = g.copy() + # We merge nodes and ensure that the minimum weight is chosen + # when there is a conflict. g.merge_nodes(0, 2) assert g.edge[1][2]['weight'] == 10 assert g.edge[2][3]['weight'] == 30 + # We specify `max_edge` as `weight_func` as ensure that maximum + # weight is chosen in case on conflict gc.merge_nodes(0, 2, weight_func=max_edge) assert gc.edge[1][2]['weight'] == 20 assert gc.edge[2][3]['weight'] == 40 From 6abf194dd668dc295f4f9652b5e256d5560f9633 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 28 Jun 2014 21:02:25 +0530 Subject: [PATCH 0095/1122] Use sets instead of dictionaries in merge_nodes --- skimage/graph/rag.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 69eed104..39bd0a3e 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -63,17 +63,8 @@ class RAG(nx.Graph): The dict of keyword arguments passed to the `weight_func`. """ - neighbors = self.adj[src].copy() - neighbors.update(self.adj[dst]) - - try: - del neighbors[src] - except KeyError: - pass - try: - del neighbors[dst] - except KeyError: - pass + neighbors = (set(self.neighbors(src)) & set( + self.neighbors(dst))) - set([src, dst]) for neighbor in neighbors: w = weight_func(self, src, dst, neighbor, *extra_arguments, From d69cdb951bae4e6e6eee03c64f4851230e5ef4f2 Mon Sep 17 00:00:00 2001 From: Kevin Murray Date: Fri, 27 Jun 2014 21:32:18 +1000 Subject: [PATCH 0096/1122] [freeimage tests] Add a test w/ truncated image This tests tests the fix in the previous commit b9a7445, covering the issue noted in issue #1037. --- skimage/data/truncated.jpg | Bin 0 -> 400 bytes skimage/io/tests/test_freeimage.py | 7 +++++++ 2 files changed, 7 insertions(+) create mode 100644 skimage/data/truncated.jpg diff --git a/skimage/data/truncated.jpg b/skimage/data/truncated.jpg new file mode 100644 index 0000000000000000000000000000000000000000..11bb8140594261069f318623950704d8a075f9be GIT binary patch literal 400 zcmex=iF;N$`UAd82aiwDF383NJD#LCRf%Eivc4pu@E@&5pWAO}MVLkcsa5(ASU zBeNjm|04|YKzFi&odL?6mQqXwbzED#l4gO`Kd};u4Zls%q*Qnp!5NX66=_R?aT2 zZtfnQUcn)uVc`*xQOPN(Y3Ui6S;Zx#W#tu>Rn0A}ZS5VMU6UqHnL2IyjG40*Enc#8 z+42=DS8dw7W$U)>J9h3mboj{8W5-XNJay^vm8;jT-?(|};iJb-o<4j2;^nK4pFV&2 Q`tAFVpT9u Date: Fri, 27 Jun 2014 21:42:09 +1000 Subject: [PATCH 0097/1122] [test_freeimage.py] remove extraneous statements --- skimage/io/tests/test_freeimage.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/skimage/io/tests/test_freeimage.py b/skimage/io/tests/test_freeimage.py index f0d19ab3..38ba725f 100644 --- a/skimage/io/tests/test_freeimage.py +++ b/skimage/io/tests/test_freeimage.py @@ -39,8 +39,6 @@ def test_imread(): @skipif(not FI_available) def test_imread_truncated_jpg(): - raised = False - raised = True assert_raises(RuntimeError, sio.imread, os.path.join(si.data_dir, 'truncated.jpg')) From 83d0717986f66b47ecc323328befdcb50a67fe54 Mon Sep 17 00:00:00 2001 From: Kevin Murray Date: Mon, 30 Jun 2014 18:47:31 +1000 Subject: [PATCH 0098/1122] [test_freeimage] Allow ValueError to be raised With the truncated image, sometimes ValueError is raised. This allows either RuntimeError (i.e. libfreeimage error) or ValueError to be raised. --- skimage/io/tests/test_freeimage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/io/tests/test_freeimage.py b/skimage/io/tests/test_freeimage.py index 38ba725f..4e64bd16 100644 --- a/skimage/io/tests/test_freeimage.py +++ b/skimage/io/tests/test_freeimage.py @@ -39,7 +39,7 @@ def test_imread(): @skipif(not FI_available) def test_imread_truncated_jpg(): - assert_raises(RuntimeError, + assert_raises((RuntimeError, ValueError), sio.imread, os.path.join(si.data_dir, 'truncated.jpg')) From 4160a67a42829d4c8322a108f4bf6d5ec77b7954 Mon Sep 17 00:00:00 2001 From: Kevin Murray Date: Mon, 30 Jun 2014 19:35:35 +1000 Subject: [PATCH 0099/1122] [skimage.io.tests] add truncated jpg imread tests --- skimage/io/tests/test_imread.py | 5 +++++ skimage/io/tests/test_pil.py | 4 ++++ skimage/io/tests/test_simpleitk.py | 6 ++++++ 3 files changed, 15 insertions(+) diff --git a/skimage/io/tests/test_imread.py b/skimage/io/tests/test_imread.py index afb9eac9..206b800d 100644 --- a/skimage/io/tests/test_imread.py +++ b/skimage/io/tests/test_imread.py @@ -37,6 +37,11 @@ def test_imread_palette(): img = imread(os.path.join(data_dir, 'palette_color.png')) assert img.ndim == 3 +@skipif(not imread_available) +def test_imread_truncated_jpg(): + assert_raises((RuntimeError, ValueError), + sio.imread, + os.path.join(si.data_dir, 'truncated.jpg')) @skipif(not imread_available) def test_bilevel(): diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index 243d2fd1..89147fd6 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -95,6 +95,10 @@ def test_repr_png(): assert np.all(original_img == round_trip) +@skipif(not PIL_available) +def test_imread_truncated_jpg(): + assert_raises((IOError, ValueError), imread, + os.path.join(data_dir, 'truncated.jpg')) # Big endian images not correctly loaded for PIL < 1.1.7 # Renable test when PIL 1.1.7 is more common. diff --git a/skimage/io/tests/test_simpleitk.py b/skimage/io/tests/test_simpleitk.py index bf3d3614..89f7416f 100644 --- a/skimage/io/tests/test_simpleitk.py +++ b/skimage/io/tests/test_simpleitk.py @@ -51,6 +51,12 @@ def test_bilevel(): np.testing.assert_array_equal(img, expected) +@skipif(not sitk_available) +def test_imread_truncated_jpg(): + assert_raises((RuntimeError, ValueError), + sio.imread, + os.path.join(si.data_dir, 'truncated.jpg')) + @skipif(not sitk_available) def test_imread_uint16(): expected = np.load(os.path.join(data_dir, 'chessboard_GRAY_U8.npy')) From 486f935db8f8f6e424524905826fb3874cc48a58 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 2 Jul 2014 00:19:11 +0530 Subject: [PATCH 0100/1122] Rename methods and edit docstrings --- ...ag_meancolor.py => plot_rag_mean_color.py} | 6 +- skimage/graph/__init__.py | 8 +-- skimage/graph/graph_cut.py | 9 ++- skimage/graph/rag.py | 67 ++++++++++--------- skimage/graph/tests/test_rag.py | 4 +- 5 files changed, 50 insertions(+), 44 deletions(-) rename doc/examples/{plot_rag_meancolor.py => plot_rag_mean_color.py} (77%) diff --git a/doc/examples/plot_rag_meancolor.py b/doc/examples/plot_rag_mean_color.py similarity index 77% rename from doc/examples/plot_rag_meancolor.py rename to doc/examples/plot_rag_mean_color.py index 11aca77c..7a1d24b9 100644 --- a/doc/examples/plot_rag_meancolor.py +++ b/doc/examples/plot_rag_mean_color.py @@ -3,7 +3,7 @@ RAG Thresholding ================ -This examples constructs a Region Adjacency Graph (RAG) and merges regions +This example constructs a Region Adjacency Graph (RAG) and merges regions which are similar in color. We construct a RAG and define edges as the difference in mean color. We then join regions with similar mean color. @@ -18,8 +18,8 @@ img = data.coffee() labels1 = segmentation.slic(img, compactness=30, n_segments=400) out1 = color.label2rgb(labels1, img, kind='avg') -g = graph.rag_meancolor(img, labels1) -labels2 = graph.threshold_cut(labels1, g, 30) +g = graph.rag_mean_color(img, labels1) +labels2 = graph.cut_threshold(labels1, g, 30) out2 = color.label2rgb(labels2, img, kind='avg') plt.figure() diff --git a/skimage/graph/__init__.py b/skimage/graph/__init__.py index 90da2088..ade8326a 100644 --- a/skimage/graph/__init__.py +++ b/skimage/graph/__init__.py @@ -1,7 +1,7 @@ from .spath import shortest_path from .mcp import MCP, MCP_Geometric, MCP_Connect, MCP_Flexible, route_through_array -from .rag import rag_meancolor -from .graph_cut import threshold_cut +from .rag import rag_mean_color +from .graph_cut import cut_threshold __all__ = ['shortest_path', 'MCP', @@ -9,5 +9,5 @@ __all__ = ['shortest_path', 'MCP_Connect', 'MCP_Flexible', 'route_through_array', - 'rag_meancolor', - 'threshold_cut'] + 'rag_mean_color', + 'cut_threshold'] diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 165091b1..a870ff9e 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -2,7 +2,7 @@ import networkx as nx import numpy as np -def threshold_cut(labels, rag, thresh): +def cut_threshold(labels, rag, thresh): """Combine regions seperated by weight less than threshold. Given an image's labels and its RAG, output new labels by @@ -16,8 +16,8 @@ def threshold_cut(labels, rag, thresh): rag : RAG The region adjacency graph. thresh : float - The threshold, regions with edge weights less than this - are combined. + The threshold. Regions connected by edges with smaller weights are + combined. Returns ------- @@ -46,6 +46,9 @@ def threshold_cut(labels, rag, thresh): comps = nx.connected_components(rag) + # We construct an array which can map old labels to the new ones. + # All the labels within a connected component are assigned to a single + # label in the output. map_array = np.arange(labels.max() + 1, dtype=labels.dtype) for i, nodes in enumerate(comps): for node in nodes: diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 39bd0a3e..e20f7c5a 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -4,18 +4,18 @@ from scipy.ndimage import filters from scipy import ndimage as nd -def min_weight(g, src, dst, n): +def min_weight(graph, src, dst, n): """Callback to handle merging nodes by choosing minimum weight. Returns either the weight between (`src`, `n`) or (`dst`, `n`) - in `g` or the minumum of the two when both exist. + in `graph` or the minumum of the two when both exist. Parameters ---------- - g : RAG + graph : RAG The graph under consideration. src, dst : int - The verices in `g` to be merged. + The verices in `graph` to be merged. n : int A neighbor of `src` or `dst` or both. @@ -28,8 +28,8 @@ def min_weight(g, src, dst, n): """ # cover the cases where n only has edge to either `src` or `dst` - w1 = g[n].get(src, {'weight': np.inf})['weight'] - w2 = g[n].get(dst, {'weight': np.inf})['weight'] + w1 = graph[n].get(src, {'weight': np.inf})['weight'] + w2 = graph[n].get(dst, {'weight': np.inf})['weight'] return min(w1, w2) @@ -50,7 +50,7 @@ class RAG(nx.Graph): Parameters ---------- src, dst : int - Nodes to be merged. The resulting node will have ID `dst`. + Nodes to be merged. weight_func : callable, optional Function to decide edge weight of edges incident on the new node. For each neighbor `n` for `src and `dst`, `weight_func` will be @@ -63,8 +63,9 @@ class RAG(nx.Graph): The dict of keyword arguments passed to the `weight_func`. """ - neighbors = (set(self.neighbors(src)) & set( - self.neighbors(dst))) - set([src, dst]) + src_nbrs = set(self.neighbors(src)) + dst_nbrs = set(self.neighbors(dst)) + neighbors = (src_nbrs & dst_nbrs) - set([src, dst]) for neighbor in neighbors: w = weight_func(self, src, dst, neighbor, *extra_arguments, @@ -75,7 +76,7 @@ class RAG(nx.Graph): self.remove_node(src) -def _add_edge_filter(values, g): +def _add_edge_filter(values, graph): """Create edge in `g` between the first element of `values` and the rest. Add an edge between the first element in `values` and @@ -86,31 +87,32 @@ def _add_edge_filter(values, g): ---------- values : array The array to process. - g : RAG + graph : RAG The graph to add edges in. Returns ------- 0 : int - Always returns 0. + Always returns 0. The return value is required so that `generic_fitler` + can put it in the output array. """ values = values.astype(int) current = values[0] for value in values[1:]: - g.add_edge(current, value) + graph.add_edge(current, value) return 0 -def rag_meancolor(image, labels, connectivity=2): +def rag_mean_color(image, labels, connectivity=2): """Compute the Region Adjacency Graph using mean colors. Given an image and its initial segmentation, this method constructs the corresponsing Region Adjacency Graph (RAG). Each node in the RAG - represents a set pixels within `image` with the same - label in `labels`. The weight between two adjacent regions is the - difference in their mean color. + represents a set of pixels within `image` with the same label in `labels`. + The weight between two adjacent regions is the difference in their mean + color. Parameters ---------- @@ -145,7 +147,7 @@ def rag_meancolor(image, labels, connectivity=2): http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.11.5274 """ - g = RAG() + graph = RAG() # The footprint is constructed in such a way that the first # element in the array being passed to _add_edge_filter is @@ -173,24 +175,25 @@ def rag_meancolor(image, labels, connectivity=2): footprint=fp, mode='nearest', output=np.zeros(labels.shape, dtype=np.uint8), - extra_arguments=(g,)) + extra_arguments=(graph,)) - for n in g: - g.node[n].update({'labels': [n], - 'pixel count': 0, - 'total color': np.array([0, 0, 0], dtype=np.double)}) + for n in graph: + graph.node[n].update({'labels': [n], + 'pixel count': 0, + 'total color': np.array([0, 0, 0], + dtype=np.double)}) for index in np.ndindex(labels.shape): current = labels[index] - g.node[current]['pixel count'] += 1 - g.node[current]['total color'] += image[index] + graph.node[current]['pixel count'] += 1 + graph.node[current]['total color'] += image[index] - for n in g: - g.node[n]['mean color'] = (g.node[n]['total color'] / - g.node[n]['pixel count']) + for n in graph: + graph.node[n]['mean color'] = (graph.node[n]['total color'] / + graph.node[n]['pixel count']) - for x, y in g.edges_iter(): - diff = g.node[x]['mean color'] - g.node[y]['mean color'] - g[x][y]['weight'] = np.linalg.norm(diff) + for x, y in graph.edges_iter(): + diff = graph.node[x]['mean color'] - graph.node[y]['mean color'] + graph[x][y]['weight'] = np.linalg.norm(diff) - return g + return graph diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 37c31887..7b4551f4 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -56,8 +56,8 @@ def test_threshold_cut(): labels[50:, :50] = 2 labels[50:, 50:] = 3 - rag = graph.rag_meancolor(img, labels) - new_labels = graph.threshold_cut(labels, rag, 10) + rag = graph.rag_mean_color(img, labels) + new_labels = graph.cut_threshold(labels, rag, 10) # Two labels assert new_labels.max() == 1 From 99a45baca823400fcda777e644069ee797df397b Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 2 Jul 2014 00:21:47 +0530 Subject: [PATCH 0101/1122] Changes to plot_rag.py --- doc/examples/plot_rag.py | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/doc/examples/plot_rag.py b/doc/examples/plot_rag.py index 203f9454..f46d86af 100644 --- a/doc/examples/plot_rag.py +++ b/doc/examples/plot_rag.py @@ -3,13 +3,14 @@ Region Adjacency Graphs ======================= -This example demonstrates the use of `merge_nodes` function of a Region -Adjacency Graph (RAG). When a new node is formed by merging two nodes, the edge -weight of all the edges incident on it can be updated by a user defined -function `weight_func`. +This example demonstrates the use of the `merge_nodes` function of a Region +Adjacency Graph (RAG).The `RAG` class represents a undirected weighted graph +which inherits from `networx.graph` class. When a new node is formed by merging +two nodes, the edge weight of all the edges incident on the resulting node can +be updated by a user defined function `weight_func`. -The default behaviour is to use the smaller edge weight incase of a conflict. -The example below also shows how to use a custom function to take the larger +The default behaviour is to use the smaller edge weight in case of a conflict. +The example below also shows how to use a custom function to select the larger weight instead. """ @@ -20,12 +21,37 @@ import numpy as np def max_edge(g, src, dst, n): + """Callback to handle merging nodes by choosing maximum weight. + + Returns either the weight between (`src`, `n`) or (`dst`, `n`) + in `g` or the maximum of the two when both exist. + + Parameters + ---------- + g : RAG + The graph under consideration. + src, dst : int + The verices in `g` to be merged. + n : int + A neighbor of `src` or `dst` or both. + + Returns + ------- + weight : float + The weight between (`src`, `n`) or (`dst`, `n`) in `g` or the + maximum of the two when both exist. + + """ + w1 = g[n].get(src, {'weight': -np.inf})['weight'] w2 = g[n].get(dst, {'weight': -np.inf})['weight'] return max(w1, w2) def display(g, title): + """Displays a graph with the given title. + + """ pos = nx.circular_layout(g) plt.figure() plt.title(title) From 6003ab2c7dd14361084676f3e0d7cd6d2ed13b34 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 2 Jul 2014 00:31:01 +0530 Subject: [PATCH 0102/1122] Typos --- doc/examples/plot_rag.py | 2 +- skimage/graph/rag.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/examples/plot_rag.py b/doc/examples/plot_rag.py index f46d86af..9dd3ca76 100644 --- a/doc/examples/plot_rag.py +++ b/doc/examples/plot_rag.py @@ -5,7 +5,7 @@ Region Adjacency Graphs This example demonstrates the use of the `merge_nodes` function of a Region Adjacency Graph (RAG).The `RAG` class represents a undirected weighted graph -which inherits from `networx.graph` class. When a new node is formed by merging +which inherits from `networkx.graph` class. When a new node is formed by merging two nodes, the edge weight of all the edges incident on the resulting node can be updated by a user defined function `weight_func`. diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index e20f7c5a..7aa65261 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -22,7 +22,7 @@ def min_weight(graph, src, dst, n): Returns ------- weight : float - The weight between (`src`, `n`) or (`dst`, `n`) in `g` or the + The weight between (`src`, `n`) or (`dst`, `n`) in `graph` or the minumum of the two when both exist. """ From 8e5c5d6cf4f90a4389860ff9ab937cf2224200cf Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Wed, 2 Jul 2014 00:56:48 +0200 Subject: [PATCH 0103/1122] Add OSX wheel generation instructions and upload script --- RELEASE.txt | 4 ++++ tools/osx_wheel_upload.sh | 30 ++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100755 tools/osx_wheel_upload.sh diff --git a/RELEASE.txt b/RELEASE.txt index b56b5bb8..5a7cfd97 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -34,6 +34,10 @@ How to make a new release of ``skimage`` python setup.py register python setup.py sdist upload + Go to https://travis-ci.org/scikit-image/scikit-image-wheels, + select the "Current" tab, and click (on the right) on the "Restart Build" + icon. After that finished, execute ``tools/osx_wheel_upload.sh``. + - Increase the version number - In ``setup.py``, set to ``0.Xdev``. diff --git a/tools/osx_wheel_upload.sh b/tools/osx_wheel_upload.sh new file mode 100755 index 00000000..411aefb7 --- /dev/null +++ b/tools/osx_wheel_upload.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Script to download / check and upload scikit_image wheels for release +RACKSPACE_URL=http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com +if [ "`which twine`" == "" ]; then + echo "twine not on path; need to pip install twine?" + exit 1 +fi +cd scikit-image +SK_VERSION=`git describe --tags` +if [ "${SK_VERSION:0:1}" != 'v' ]; then + echo "scikit image version $SK_VERSION does not start with 'v'" + exit 1 +fi +WHEEL_HEAD="scikit_image-${SK_VERSION:1}" +WHEEL_TAIL="macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.whl" +mkdir -p wheels +cd wheels +rm -rf *.whl +for py_tag in cp27-none cp33-cp33m cp34-cp34m +do + wheel_name="$WHEEL_HEAD-$py_tag-$WHEEL_TAIL" + wheel_url="${RACKSPACE_URL}/${wheel_name}" + curl -O $wheel_url + if [ "$?" != "0" ]; then + echo "Failed downloading $wheel_url; check travis build?" + exit $? + fi +done +cd .. +twine upload wheels/*.whl From 6926b6f9ec4049323605dd09e153d7db0fd5f3cc Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Wed, 2 Jul 2014 01:06:43 +0200 Subject: [PATCH 0104/1122] Update wheel URL --- tools/osx_wheel_upload.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/osx_wheel_upload.sh b/tools/osx_wheel_upload.sh index 411aefb7..c557b56e 100755 --- a/tools/osx_wheel_upload.sh +++ b/tools/osx_wheel_upload.sh @@ -1,6 +1,6 @@ #!/bin/bash # Script to download / check and upload scikit_image wheels for release -RACKSPACE_URL=http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com +RACKSPACE_URL=http://wheels.scikit-image.org if [ "`which twine`" == "" ]; then echo "twine not on path; need to pip install twine?" exit 1 From 9002eebfcc7e871b7cd40ce4e796ff20c286d902 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Wed, 2 Jul 2014 01:40:31 +0200 Subject: [PATCH 0105/1122] Get tags from current repo --- tools/osx_wheel_upload.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/osx_wheel_upload.sh b/tools/osx_wheel_upload.sh index c557b56e..7d757379 100755 --- a/tools/osx_wheel_upload.sh +++ b/tools/osx_wheel_upload.sh @@ -5,7 +5,6 @@ if [ "`which twine`" == "" ]; then echo "twine not on path; need to pip install twine?" exit 1 fi -cd scikit-image SK_VERSION=`git describe --tags` if [ "${SK_VERSION:0:1}" != 'v' ]; then echo "scikit image version $SK_VERSION does not start with 'v'" @@ -20,6 +19,7 @@ for py_tag in cp27-none cp33-cp33m cp34-cp34m do wheel_name="$WHEEL_HEAD-$py_tag-$WHEEL_TAIL" wheel_url="${RACKSPACE_URL}/${wheel_name}" + echo "Fetching $wheel_url" curl -O $wheel_url if [ "$?" != "0" ]; then echo "Failed downloading $wheel_url; check travis build?" From b6c8ee9546b34fff3272d21b10f8985fc01b204e Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Wed, 2 Jul 2014 14:32:00 +0200 Subject: [PATCH 0106/1122] Indicate waiting time before wheels become available --- RELEASE.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/RELEASE.txt b/RELEASE.txt index 5a7cfd97..63de588f 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -34,9 +34,11 @@ How to make a new release of ``skimage`` python setup.py register python setup.py sdist upload - Go to https://travis-ci.org/scikit-image/scikit-image-wheels, - select the "Current" tab, and click (on the right) on the "Restart Build" - icon. After that finished, execute ``tools/osx_wheel_upload.sh``. + Go to https://travis-ci.org/scikit-image/scikit-image-wheels, select the + "Current" tab, and click (on the right) on the "Restart Build" icon. After + the wheels become available at http://wheels.scikit-image.org/ (approx 15 + mins), execute ``tools/osx_wheel_upload.sh``. Note that, if you rebuild the + same wheels, the Rackspace server may only update in another 15 minutes. - Increase the version number From 0fca535caa5d19871b2ccf0fef2c7a9668278557 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Wed, 2 Jul 2014 18:30:18 +0200 Subject: [PATCH 0107/1122] Remember to update stable docs link. Check timestamp on wheels before posting. --- RELEASE.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/RELEASE.txt b/RELEASE.txt index 63de588f..836d2222 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -19,6 +19,7 @@ How to make a new release of ``skimage`` place. Double check ``random.js``, otherwise the skimage.org front page gets broken! - Build using ``make gh-pages``. + - Update the symlink to ``stable``. - Push upstream: ``git push origin gh-pages`` in ``doc/gh-pages``. - Add the version number as a tag in git:: @@ -38,7 +39,10 @@ How to make a new release of ``skimage`` "Current" tab, and click (on the right) on the "Restart Build" icon. After the wheels become available at http://wheels.scikit-image.org/ (approx 15 mins), execute ``tools/osx_wheel_upload.sh``. Note that, if you rebuild the - same wheels, the Rackspace server may only update in another 15 minutes. + same wheels, it can take up to 15 minutes for the the files in the http + directory to update to the versions that Travis-CI uploaded. You may want to + check the timestamps in the http directory listing to check that you will get + the latest version. - Increase the version number From c1cf8e63472769e01efe033ec1f74ddcc8187669 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 2 Jul 2014 23:13:17 +0530 Subject: [PATCH 0108/1122] Got doctest to pass and docstring changes --- doc/examples/plot_rag.py | 8 ++++---- skimage/graph/rag.py | 11 +++++++---- skimage/graph/tests/test_rag.py | 5 +++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/doc/examples/plot_rag.py b/doc/examples/plot_rag.py index 9dd3ca76..c3192ca2 100644 --- a/doc/examples/plot_rag.py +++ b/doc/examples/plot_rag.py @@ -4,10 +4,10 @@ Region Adjacency Graphs ======================= This example demonstrates the use of the `merge_nodes` function of a Region -Adjacency Graph (RAG).The `RAG` class represents a undirected weighted graph -which inherits from `networkx.graph` class. When a new node is formed by merging -two nodes, the edge weight of all the edges incident on the resulting node can -be updated by a user defined function `weight_func`. +Adjacency Graph (RAG). The `RAG` class represents a undirected weighted graph +which inherits from `networkx.graph` class. When a new node is formed by +merging two nodes, the edge weight of all the edges incident on the resulting +node can be updated by a user defined function `weight_func`. The default behaviour is to use the smaller edge weight in case of a conflict. The example below also shows how to use a custom function to select the larger diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 7aa65261..807e8cd3 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -28,8 +28,9 @@ def min_weight(graph, src, dst, n): """ # cover the cases where n only has edge to either `src` or `dst` - w1 = graph[n].get(src, {'weight': np.inf})['weight'] - w2 = graph[n].get(dst, {'weight': np.inf})['weight'] + default = {'weight': np.inf} + w1 = graph[n].get(src, default)['weight'] + w2 = graph[n].get(dst, default)['weight'] return min(w1, w2) @@ -55,7 +56,9 @@ class RAG(nx.Graph): Function to decide edge weight of edges incident on the new node. For each neighbor `n` for `src and `dst`, `weight_func` will be called as follows: `weight_func(src, dst, n, *extra_arguments, - **extra_keywords)` + **extra_keywords)`. `src`, `dst` and `n` are IDs of vertices in a + .. py:class:: RAG object which is in turn a subclass of + `networkx.Graph`. extra_arguments : sequence, optional The sequence of extra positional arguments passed to `weight_func`. @@ -138,7 +141,7 @@ def rag_mean_color(image, labels, connectivity=2): >>> from skimage import data, graph, segmentation >>> img = data.lena() >>> labels = segmentation.slic(img) - >>> rag = graph.rag_meancolor(img, labels) + >>> rag = graph.rag_mean_color(img, labels) References ---------- diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 7b4551f4..af7481eb 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -3,8 +3,9 @@ from skimage import graph def max_edge(g, src, dst, n): - w1 = g[n].get(src, {'weight': -np.inf})['weight'] - w2 = g[n].get(dst, {'weight': -np.inf})['weight'] + default = {'weight': -np.inf} + w1 = g[n].get(src, default)['weight'] + w2 = g[n].get(dst, default)['weight'] return max(w1, w2) From e558854deccd91e047edbd3e483df468f41ed940 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 2 Jul 2014 23:28:14 +0530 Subject: [PATCH 0109/1122] docstring change --- skimage/graph/rag.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 807e8cd3..17c8cab8 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -56,8 +56,8 @@ class RAG(nx.Graph): Function to decide edge weight of edges incident on the new node. For each neighbor `n` for `src and `dst`, `weight_func` will be called as follows: `weight_func(src, dst, n, *extra_arguments, - **extra_keywords)`. `src`, `dst` and `n` are IDs of vertices in a - .. py:class:: RAG object which is in turn a subclass of + **extra_keywords)`. `src`, `dst` and `n` are IDs of vertices in the + RAG object which is in turn a subclass of `networkx.Graph`. extra_arguments : sequence, optional The sequence of extra positional arguments passed to From a4bf278c5b811acea34fcdabae1bbb5dbe49c4ce Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 3 Jul 2014 00:26:22 +0530 Subject: [PATCH 0110/1122] Doctest Passing --- skimage/graph/graph_cut.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index a870ff9e..91d64aec 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -29,8 +29,8 @@ def cut_threshold(labels, rag, thresh): >>> from skimage import data, graph, segmentation >>> img = data.lena() >>> labels = segmentation.slic(img) - >>> rag = graph.rag_meancolor(img, labels) - >>> new_labels = graph.threshold_cut(labels, rag, 10) + >>> rag = graph.rag_mean_color(img, labels) + >>> new_labels = graph.cut_threshold(labels, rag, 10) References ---------- From fb706d12d575499e5886965f3cfd9f94d74c2e0d Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 3 Jul 2014 01:55:09 +0530 Subject: [PATCH 0111/1122] made RAG public and hyperlinked networkx.Graph documentation --- skimage/graph/__init__.py | 5 +++-- skimage/graph/rag.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/skimage/graph/__init__.py b/skimage/graph/__init__.py index ade8326a..68c1206c 100644 --- a/skimage/graph/__init__.py +++ b/skimage/graph/__init__.py @@ -1,6 +1,6 @@ from .spath import shortest_path from .mcp import MCP, MCP_Geometric, MCP_Connect, MCP_Flexible, route_through_array -from .rag import rag_mean_color +from .rag import rag_mean_color, RAG from .graph_cut import cut_threshold __all__ = ['shortest_path', @@ -10,4 +10,5 @@ __all__ = ['shortest_path', 'MCP_Flexible', 'route_through_array', 'rag_mean_color', - 'cut_threshold'] + 'cut_threshold', + 'RAG'] diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 17c8cab8..aacba2ba 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -37,7 +37,8 @@ def min_weight(graph, src, dst, n): class RAG(nx.Graph): """ - The Region Adjacency Graph (RAG) of an image. + The Region Adjacency Graph (RAG) of an image, subclasses + `networx.Graph `_ """ def merge_nodes(self, src, dst, weight_func=min_weight, extra_arguments=[], From 351e7697331548cf6a19ef1ed8a512a95424517c Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 3 Jul 2014 09:54:34 +0530 Subject: [PATCH 0112/1122] Docstring change --- doc/examples/plot_rag.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/examples/plot_rag.py b/doc/examples/plot_rag.py index c3192ca2..b26a266d 100644 --- a/doc/examples/plot_rag.py +++ b/doc/examples/plot_rag.py @@ -49,9 +49,7 @@ def max_edge(g, src, dst, n): def display(g, title): - """Displays a graph with the given title. - - """ + """Displays a graph with the given title.""" pos = nx.circular_layout(g) plt.figure() plt.title(title) From df1d61e4618ab8ffb2a2a2d8567d8be3bc069bbd Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 3 Jul 2014 09:55:17 +0530 Subject: [PATCH 0113/1122] Spelling correction. --- skimage/graph/rag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index aacba2ba..84721736 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -97,7 +97,7 @@ def _add_edge_filter(values, graph): Returns ------- 0 : int - Always returns 0. The return value is required so that `generic_fitler` + Always returns 0. The return value is required so that `generic_filter` can put it in the output array. """ From 7a074d5dd165536cb05fd87ea94a29510edac1a7 Mon Sep 17 00:00:00 2001 From: Charles Harris Date: Fri, 4 Jul 2014 20:04:32 -0600 Subject: [PATCH 0114/1122] BUG: Fix slicing error revealed by numpy 1.9.0. Numpy 1.9.0 no longer allows oversize data to be assigned to a boolean indexed 1-d array by discarding excess elements. Closes #1050. --- skimage/segmentation/_join.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/segmentation/_join.py b/skimage/segmentation/_join.py index 7b6d2a3e..5cceeb5b 100644 --- a/skimage/segmentation/_join.py +++ b/skimage/segmentation/_join.py @@ -124,7 +124,7 @@ def relabel_sequential(label_field, offset=1): if m == len(labels0): # nothing to do, already 1...n labels return label_field, labels, labels forward_map = np.zeros(m + 1, int) - forward_map[labels0] = np.arange(offset, offset + len(labels0) + 1) + forward_map[labels0] = np.arange(offset, offset + len(labels0)) if not (labels == 0).any(): labels = np.concatenate(([0], labels)) inverse_map = np.zeros(offset - 1 + len(labels), dtype=np.intp) From 5ef3f95d71d384b7206f02a802e3430b4f6f77c2 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sat, 5 Jul 2014 12:49:42 -0500 Subject: [PATCH 0115/1122] Add random seeds to tests per #1044 --- skimage/color/tests/test_colorconv.py | 2 ++ skimage/feature/tests/test_censure.py | 1 + skimage/filter/rank/tests/test_rank.py | 2 ++ skimage/io/tests/test_freeimage.py | 2 ++ skimage/io/tests/test_imread.py | 2 ++ skimage/io/tests/test_pil.py | 2 ++ skimage/io/tests/test_plugin_util.py | 2 ++ skimage/io/tests/test_simpleitk.py | 2 ++ skimage/io/tests/test_tifffile.py | 2 ++ skimage/morphology/tests/test_ccomp.py | 3 +++ skimage/transform/tests/test_integral.py | 1 + skimage/transform/tests/test_warps.py | 3 +++ 12 files changed, 24 insertions(+) diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index 4bc01e78..bbc1c61b 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -43,6 +43,8 @@ from skimage import data_dir, data import colorsys +np.random.seed(0) + def test_guess_spatial_dimensions(): im1 = np.zeros((5, 5)) diff --git a/skimage/feature/tests/test_censure.py b/skimage/feature/tests/test_censure.py index 53c1c59b..6a4aed9c 100644 --- a/skimage/feature/tests/test_censure.py +++ b/skimage/feature/tests/test_censure.py @@ -5,6 +5,7 @@ from skimage.feature import CENSURE img = moon() +np.random.seed(0) def test_censure_on_rectangular_images(): diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py index 72f9afe6..d8122ba7 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filter/rank/tests/test_rank.py @@ -6,6 +6,8 @@ from skimage import data, util from skimage.morphology import cmorph, disk from skimage.filter import rank +np.random.seed(0) + def test_random_sizes(): # make sure the size is not a problem diff --git a/skimage/io/tests/test_freeimage.py b/skimage/io/tests/test_freeimage.py index 4e64bd16..dd34828f 100644 --- a/skimage/io/tests/test_freeimage.py +++ b/skimage/io/tests/test_freeimage.py @@ -14,6 +14,8 @@ try: except RuntimeError: FI_available = False +np.random.seed(0) + def setup_module(self): """The effect of the `plugin.use` call may be overridden by later imports. diff --git a/skimage/io/tests/test_imread.py b/skimage/io/tests/test_imread.py index 206b800d..b4886970 100644 --- a/skimage/io/tests/test_imread.py +++ b/skimage/io/tests/test_imread.py @@ -16,6 +16,8 @@ except ImportError: else: imread_available = True +np.random.seed(0) + def teardown(): reset_plugins() diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index 89147fd6..1b534aa5 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -21,6 +21,8 @@ except ImportError: else: PIL_available = True +np.random.seed(0) + def teardown(): reset_plugins() diff --git a/skimage/io/tests/test_plugin_util.py b/skimage/io/tests/test_plugin_util.py index 41f28339..7b337a94 100644 --- a/skimage/io/tests/test_plugin_util.py +++ b/skimage/io/tests/test_plugin_util.py @@ -3,6 +3,8 @@ from skimage.io._plugins.util import prepare_for_display, WindowManager from numpy.testing import * import numpy as np +np.random.seed(0) + class TestPrepareForDisplay: def test_basic(self): diff --git a/skimage/io/tests/test_simpleitk.py b/skimage/io/tests/test_simpleitk.py index 89f7416f..51f94986 100644 --- a/skimage/io/tests/test_simpleitk.py +++ b/skimage/io/tests/test_simpleitk.py @@ -15,6 +15,8 @@ except ImportError: else: sitk_available = True +np.random.seed(0) + def teardown(): reset_plugins() diff --git a/skimage/io/tests/test_tifffile.py b/skimage/io/tests/test_tifffile.py index 0f128bf1..00c7f41c 100644 --- a/skimage/io/tests/test_tifffile.py +++ b/skimage/io/tests/test_tifffile.py @@ -15,6 +15,8 @@ try: except ImportError: TF_available = False +np.random.seed(0) + def teardown(): sio.reset_plugins() diff --git a/skimage/morphology/tests/test_ccomp.py b/skimage/morphology/tests/test_ccomp.py index e934a5b7..48c8a850 100644 --- a/skimage/morphology/tests/test_ccomp.py +++ b/skimage/morphology/tests/test_ccomp.py @@ -5,6 +5,9 @@ from skimage.morphology import label from warnings import catch_warnings from skimage._shared.utils import skimage_deprecation +np.random.seed(0) + + class TestConnectedComponents: def setup(self): self.x = np.array([[0, 0, 3, 2, 1, 9], diff --git a/skimage/transform/tests/test_integral.py b/skimage/transform/tests/test_integral.py index 4a641e71..f3232512 100644 --- a/skimage/transform/tests/test_integral.py +++ b/skimage/transform/tests/test_integral.py @@ -5,6 +5,7 @@ from skimage.transform import integral_image, integrate x = (np.random.random((50, 50)) * 255).astype(np.uint8) s = integral_image(x) +np.random.seed(0) def test_validity(): diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 07054ab7..66c867df 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -12,6 +12,9 @@ from skimage import transform as tf, data, img_as_float from skimage.color import rgb2gray +np.random.seed(0) + + def test_warp_tform(): x = np.zeros((5, 5), dtype=np.double) x[2, 2] = 1 From 35dcdd53159f0899f2b8f6f1fa65bad393926f4a Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sun, 6 Jul 2014 07:49:36 -0500 Subject: [PATCH 0116/1122] Move random seed prior to first random() call --- skimage/transform/tests/test_integral.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/tests/test_integral.py b/skimage/transform/tests/test_integral.py index f3232512..c9182a0e 100644 --- a/skimage/transform/tests/test_integral.py +++ b/skimage/transform/tests/test_integral.py @@ -3,9 +3,9 @@ from numpy.testing import assert_equal from skimage.transform import integral_image, integrate +np.random.seed(0) x = (np.random.random((50, 50)) * 255).astype(np.uint8) s = integral_image(x) -np.random.seed(0) def test_validity(): From bb732c922585073645a444dcbbccfcdf3969b744 Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Fri, 11 Jul 2014 13:50:55 -0700 Subject: [PATCH 0117/1122] Add default structuring element to morphology --- skimage/morphology/binary.py | 19 +++++++--- skimage/morphology/grey.py | 46 ++++++++++++++++++++----- skimage/morphology/selem.py | 21 +++++++++++ skimage/morphology/tests/test_binary.py | 25 ++++++++++++++ skimage/morphology/tests/test_grey.py | 26 ++++++++++++++ 5 files changed, 124 insertions(+), 13 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index 24c5c0ea..636717af 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -1,9 +1,10 @@ import warnings import numpy as np from scipy import ndimage +from .selem import _default_selem -def binary_erosion(image, selem, out=None): +def binary_erosion(image, selem=None, out=None): """Return fast binary morphological erosion of an image. This function returns the same result as greyscale erosion but performs @@ -29,6 +30,11 @@ def binary_erosion(image, selem, out=None): The result of the morphological erosion with values in ``[0, 1]``. """ + + # Default structure element + if selem is None: + selem = _default_selem(image.ndim) + selem = (selem != 0) selem_sum = np.sum(selem) @@ -45,7 +51,7 @@ def binary_erosion(image, selem, out=None): return np.equal(conv, selem_sum, out=out) -def binary_dilation(image, selem, out=None): +def binary_dilation(image, selem=None, out=None): """Return fast binary morphological dilation of an image. This function returns the same result as greyscale dilation but performs @@ -72,6 +78,11 @@ def binary_dilation(image, selem, out=None): The result of the morphological dilation with values in ``[0, 1]``. """ + + # Default structure element + if selem is None: + selem = _default_selem(image.ndim) + selem = (selem != 0) if np.sum(selem) <= 255: @@ -87,7 +98,7 @@ def binary_dilation(image, selem, out=None): return np.not_equal(conv, 0, out=out) -def binary_opening(image, selem, out=None): +def binary_opening(image, selem=None, out=None): """Return fast binary morphological opening of an image. This function returns the same result as greyscale opening but performs @@ -119,7 +130,7 @@ def binary_opening(image, selem, out=None): return out -def binary_closing(image, selem, out=None): +def binary_closing(image, selem=None, out=None): """Return fast binary morphological closing of an image. This function returns the same result as greyscale closing but performs diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 13e9dfbb..7fb1c8c9 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -1,5 +1,8 @@ import warnings from skimage import img_as_ubyte +from scipy import ndimage +from .selem import _default_selem + from . import cmorph @@ -8,7 +11,7 @@ __all__ = ['erosion', 'dilation', 'opening', 'closing', 'white_tophat', 'black_tophat'] -def erosion(image, selem, out=None, shift_x=False, shift_y=False): +def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): """Return greyscale morphological erosion of an image. Morphological erosion sets a pixel at (i,j) to the minimum over all pixels @@ -21,7 +24,7 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarrays The array to store the result of the morphology. If None is passed, a new array will be allocated. shift_x, shift_y : bool @@ -52,6 +55,10 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): """ + # Default structure element + if selem is None: + selem = _default_selem(image.ndim) + if image is out: raise NotImplementedError("In-place erosion not supported!") image = img_as_ubyte(image) @@ -60,7 +67,7 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): shift_x=shift_x, shift_y=shift_y) -def dilation(image, selem, out=None, shift_x=False, shift_y=False): +def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): """Return greyscale morphological dilation of an image. Morphological dilation sets a pixel at (i,j) to the maximum over all pixels @@ -105,6 +112,10 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): """ + # Default structure element + if selem is None: + selem = _default_selem(image.ndim) + if image is out: raise NotImplementedError("In-place dilation not supported!") image = img_as_ubyte(image) @@ -113,7 +124,7 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): shift_x=shift_x, shift_y=shift_y) -def opening(image, selem, out=None): +def opening(image, selem=None, out=None): """Return greyscale morphological opening of an image. The morphological opening on an image is defined as an erosion followed by @@ -154,7 +165,11 @@ def opening(image, selem, out=None): [0, 0, 0, 0, 0]], dtype=uint8) """ - + + # Default structure element + if selem is None: + selem = _default_selem(image.ndim) + h, w = selem.shape shift_x = True if (w % 2) == 0 else False shift_y = True if (h % 2) == 0 else False @@ -164,7 +179,7 @@ def opening(image, selem, out=None): return out -def closing(image, selem, out=None): +def closing(image, selem=None, out=None): """Return greyscale morphological closing of an image. The morphological closing on an image is defined as a dilation followed by @@ -206,6 +221,10 @@ def closing(image, selem, out=None): """ + # Default structure element + if selem is None: + selem = _default_selem(image.ndim) + h, w = selem.shape shift_x = True if (w % 2) == 0 else False shift_y = True if (h % 2) == 0 else False @@ -215,7 +234,7 @@ def closing(image, selem, out=None): return out -def white_tophat(image, selem, out=None): +def white_tophat(image, selem=None, out=None): """Return white top hat of an image. The white top hat of an image is defined as the image minus its @@ -254,7 +273,12 @@ def white_tophat(image, selem, out=None): [0, 0, 1, 0, 0], [0, 0, 0, 0, 0]], dtype=uint8) - """ + """ + + # Default structure element + if selem is None: + selem = _default_selem(image.ndim) + if image is out: raise NotImplementedError("Cannot perform white top hat in place.") @@ -263,7 +287,7 @@ def white_tophat(image, selem, out=None): return out -def black_tophat(image, selem, out=None): +def black_tophat(image, selem=None, out=None): """Return black top hat of an image. The black top hat of an image is defined as its morphological closing minus @@ -305,6 +329,10 @@ def black_tophat(image, selem, out=None): """ + # Default structure element + if selem is None: + selem = _default_selem(image.ndim) + if image is out: raise NotImplementedError("Cannot perform white top hat in place.") diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index 7e566773..24adfce6 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -4,6 +4,7 @@ """ import numpy as np +from scipy import ndimage def square(width, dtype=np.uint8): @@ -290,3 +291,23 @@ def star(a, dtype=np.uint8): selem = selem_square + selem_rotated selem[selem > 0] = 1 return selem.astype(dtype) + + +def _default_selem(ndim): + """ + Generates a cross-shaped structuring element (connectivity=1). This is the + default structuring element (selem) if no selem was specified. + + Parameters + ---------- + ndim : int + Number of dimensions of the image. + + Returns + ------- + selem : ndarray + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. + + """ + return ndimage.morphology.generate_binary_structure(ndim, 1) \ No newline at end of file diff --git a/skimage/morphology/tests/test_binary.py b/skimage/morphology/tests/test_binary.py index deab3d82..e6d80321 100644 --- a/skimage/morphology/tests/test_binary.py +++ b/skimage/morphology/tests/test_binary.py @@ -64,5 +64,30 @@ def test_out_argument(): testing.assert_(np.any(out != out_saved)) testing.assert_array_equal(out, func(img, strel)) +def test_default_selem(): + + functions = [binary.binary_erosion, binary.binary_dilation, + binary.binary_opening, binary.binary_closing] + + strel = selem.diamond(radius=1) + image = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], np.uint8) + + for function in functions: + im_expected = function(image, strel) + im_test = function(image) + yield testing.assert_array_equal, im_expected, im_test + if __name__ == '__main__': testing.run_module_suite() diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index f0099ee5..d298b1eb 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -119,7 +119,33 @@ class TestEccentricStructuringElements(): tophat = grey.black_tophat(self.white_pixel, s) assert np.all(tophat == 0) +def test_default_selem(): + functions = [grey.erosion, grey.dilation, + grey.opening, grey.closing, + grey.white_tophat, grey.black_tophat] + + strel = selem.diamond(radius=1) + image = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], np.uint8) + + for function in functions: + im_expected = function(image, strel) + im_test = function(image) + yield testing.assert_array_equal, im_expected, im_test + + class TestDTypes(): def setUp(self): From 54aff88bd019b0ed077fe2bf0b4b9bbdf545df1e Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Fri, 11 Jul 2014 14:10:18 -0700 Subject: [PATCH 0118/1122] Add docstring for default structuring element --- skimage/morphology/binary.py | 18 +++++++++++------- skimage/morphology/grey.py | 32 +++++++++++++++++++------------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index 636717af..d5e0d92b 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -18,8 +18,9 @@ def binary_erosion(image, selem=None, out=None): ---------- image : ndarray Binary input image. - selem : ndarray + selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. + If None, use cross-shaped structuring element (connectivity=1). out : ndarray of bool The array to store the result of the morphology. If None is passed, a new array will be allocated. @@ -66,9 +67,10 @@ def binary_dilation(image, selem=None, out=None): image : ndarray Binary input image. - selem : ndarray + selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray of bool + If None, use cross-shaped structuring element (connectivity=1). + out : ndarray of bool, optional The array to store the result of the morphology. If None, is passed, a new array will be allocated. @@ -113,9 +115,10 @@ def binary_opening(image, selem=None, out=None): ---------- image : ndarray Binary input image. - selem : ndarray + selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray of bool + If None, use cross-shaped structuring element (connectivity=1). + out : ndarray of bool, optional The array to store the result of the morphology. If None is passed, a new array will be allocated. @@ -145,9 +148,10 @@ def binary_closing(image, selem=None, out=None): ---------- image : ndarray Binary input image. - selem : ndarray + selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray of bool + If None, use cross-shaped structuring element (connectivity=1). + out : ndarray of bool, optional The array to store the result of the morphology. If None, is passed, a new array will be allocated. diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 7fb1c8c9..e50e1460 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -22,12 +22,13 @@ def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): ---------- image : ndarray Image array. - selem : ndarray + selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarrays + If None, use cross-shaped structuring element (connectivity=1). + out : ndarrays, optional The array to store the result of the morphology. If None is passed, a new array will be allocated. - shift_x, shift_y : bool + shift_x, shift_y : bool, optional shift structuring element about center point. This only affects eccentric structuring elements (i.e. selem with even numbered sides). @@ -79,12 +80,13 @@ def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): image : ndarray Image array. - selem : ndarray + selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + If None, use cross-shaped structuring element (connectivity=1). + out : ndarray, optional The array to store the result of the morphology. If None, is passed, a new array will be allocated. - shift_x, shift_y : bool + shift_x, shift_y : bool, optional shift structuring element about center point. This only affects eccentric structuring elements (i.e. selem with even numbered sides). @@ -136,9 +138,10 @@ def opening(image, selem=None, out=None): ---------- image : ndarray Image array. - selem : ndarray + selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + If None, use cross-shaped structuring element (connectivity=1). + out : ndarray, optional The array to store the result of the morphology. If None is passed, a new array will be allocated. @@ -191,9 +194,10 @@ def closing(image, selem=None, out=None): ---------- image : ndarray Image array. - selem : ndarray + selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + If None, use cross-shaped structuring element (connectivity=1). + out : ndarray, optional The array to store the result of the morphology. If None, is passed, a new array will be allocated. @@ -245,9 +249,10 @@ def white_tophat(image, selem=None, out=None): ---------- image : ndarray Image array. - selem : ndarray + selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + If None, use cross-shaped structuring element (connectivity=1). + out : ndarray, optional The array to store the result of the morphology. If None is passed, a new array will be allocated. @@ -299,8 +304,9 @@ def black_tophat(image, selem=None, out=None): ---------- image : ndarray Image array. - selem : ndarray + selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. + If None, use cross-shaped structuring element (connectivity=1). out : ndarray The array to store the result of the morphology. If None is passed, a new array will be allocated. From 879c2c7f36d1819b580e4977617ec40bb9e44cc4 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 11 Jul 2014 16:30:18 -0500 Subject: [PATCH 0119/1122] Add tests for intensity_range and rename parameter --- skimage/exposure/exposure.py | 14 +++++----- skimage/exposure/tests/test_exposure.py | 36 +++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index 12161c4e..ad21fa9b 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -3,7 +3,7 @@ import numpy as np from skimage import img_as_float from skimage.util.dtype import dtype_range, dtype_limits -from skimage._shared.utils import deprecated, deprecation_warning +from skimage._shared.utils import deprecation_warning __all__ = ['histogram', 'cumulative_distribution', 'equalize', @@ -136,7 +136,7 @@ def equalize_hist(image, nbins=256): return out.reshape(image.shape) -def intensity_range(image, range_values='image', zero_min=False): +def intensity_range(image, range_values='image', clip_negative=False): """Return image intensity range (min, max) based on desired value type. Parameters @@ -160,9 +160,9 @@ def intensity_range(image, range_values='image', zero_min=False): intensity range explicitly. This option is included for functions that use `intensity_range` to support all desired range types. - zero_min : bool - If True, the image dtype's min is truncated to 0. Note that this only - applies to the output range if `range_values` specifies a dtype. + clip_negative : bool + If True, clip the negative range (i.e. return 0 for min intensity) + even if the image dtype allows negative values. """ if range_values == 'dtype': range_values = image.dtype.type @@ -172,7 +172,7 @@ def intensity_range(image, range_values='image', zero_min=False): i_max = np.max(image) elif range_values in DTYPE_RANGE: i_min, i_max = DTYPE_RANGE[range_values] - if zero_min: + if clip_negative: i_min = 0 else: i_min, i_max = range_values @@ -263,7 +263,7 @@ def rescale_intensity(image, in_range='image', out_range='dtype'): deprecation_warning(msg.format(out_range)) imin, imax = intensity_range(image, in_range) - omin, omax = intensity_range(image, out_range, zero_min=(imin >= 0)) + omin, omax = intensity_range(image, out_range, clip_negative=(imin >= 0)) image = np.clip(image, imin, imax) diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py index 6471ff59..dcaf6837 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -3,9 +3,11 @@ import warnings import numpy as np from numpy.testing import assert_array_almost_equal as assert_close from numpy.testing import assert_array_equal, assert_raises + import skimage from skimage import data from skimage import exposure +from skimage.exposure.exposure import intensity_range from skimage.color import rgb2gray from skimage.util.dtype import dtype_range @@ -41,6 +43,36 @@ def check_cdf_slope(cdf): assert 0.9 < slope < 1.1 +# Test intensity range +# ==================== + + +def test_intensity_range_uint8(): + image = np.array([0, 1], dtype=np.uint8) + input_and_expected = [('image', [0, 1]), + ('dtype', [0, 255]), + ((10, 20), [10, 20])] + for range_values, expected_values in input_and_expected: + out = intensity_range(image, range_values=range_values) + yield assert_array_equal, out, expected_values + + +def test_intensity_range_float(): + image = np.array([0.1, 0.2], dtype=np.float64) + input_and_expected = [('image', [0.1, 0.2]), + ('dtype', [-1, 1]), + ((0.3, 0.4), [0.3, 0.4])] + for range_values, expected_values in input_and_expected: + out = intensity_range(image, range_values=range_values) + yield assert_array_equal, out, expected_values + + +def test_intensity_range_clipped_float(): + image = np.array([0.1, 0.2], dtype=np.float64) + out = intensity_range(image, range_values='dtype', clip_negative=True) + assert_array_equal(out, (0, 1)) + + # Test rescale intensity # ====================== @@ -134,7 +166,7 @@ def test_adapthist_grayscale(): img = rgb2gray(img) img = np.dstack((img, img, img)) adapted = exposure.equalize_adapthist(img, 10, 9, clip_limit=0.01, - nbins=128) + nbins=128) assert_almost_equal = np.testing.assert_almost_equal assert img.shape == adapted.shape assert_almost_equal(peak_snr(img, adapted), 97.531, 3) @@ -374,6 +406,6 @@ def test_adjust_inv_sigmoid_cutoff_half(): assert_array_equal(result, expected) -def test_neggative(): +def test_negative(): image = np.arange(-10, 245, 4).reshape(8, 8).astype(np.double) assert_raises(ValueError, exposure.adjust_gamma, image) From 1ef556b7104d1c57a8a47309d878553966ad0f1a Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Fri, 11 Jul 2014 14:51:57 -0700 Subject: [PATCH 0120/1122] Remove unnecessary whitespace --- skimage/morphology/binary.py | 8 ++++---- skimage/morphology/grey.py | 16 ++++++++-------- skimage/morphology/selem.py | 2 +- skimage/morphology/tests/test_binary.py | 5 +---- skimage/morphology/tests/test_grey.py | 6 +----- 5 files changed, 15 insertions(+), 22 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index d5e0d92b..5b019051 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -31,11 +31,11 @@ def binary_erosion(image, selem=None, out=None): The result of the morphological erosion with values in ``[0, 1]``. """ - + # Default structure element if selem is None: selem = _default_selem(image.ndim) - + selem = (selem != 0) selem_sum = np.sum(selem) @@ -80,11 +80,11 @@ def binary_dilation(image, selem=None, out=None): The result of the morphological dilation with values in ``[0, 1]``. """ - + # Default structure element if selem is None: selem = _default_selem(image.ndim) - + selem = (selem != 0) if np.sum(selem) <= 255: diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index e50e1460..2b4a01a8 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -59,7 +59,7 @@ def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): # Default structure element if selem is None: selem = _default_selem(image.ndim) - + if image is out: raise NotImplementedError("In-place erosion not supported!") image = img_as_ubyte(image) @@ -117,7 +117,7 @@ def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): # Default structure element if selem is None: selem = _default_selem(image.ndim) - + if image is out: raise NotImplementedError("In-place dilation not supported!") image = img_as_ubyte(image) @@ -168,11 +168,11 @@ def opening(image, selem=None, out=None): [0, 0, 0, 0, 0]], dtype=uint8) """ - + # Default structure element if selem is None: selem = _default_selem(image.ndim) - + h, w = selem.shape shift_x = True if (w % 2) == 0 else False shift_y = True if (h % 2) == 0 else False @@ -228,7 +228,7 @@ def closing(image, selem=None, out=None): # Default structure element if selem is None: selem = _default_selem(image.ndim) - + h, w = selem.shape shift_x = True if (w % 2) == 0 else False shift_y = True if (h % 2) == 0 else False @@ -279,11 +279,11 @@ def white_tophat(image, selem=None, out=None): [0, 0, 0, 0, 0]], dtype=uint8) """ - + # Default structure element if selem is None: selem = _default_selem(image.ndim) - + if image is out: raise NotImplementedError("Cannot perform white top hat in place.") @@ -338,7 +338,7 @@ def black_tophat(image, selem=None, out=None): # Default structure element if selem is None: selem = _default_selem(image.ndim) - + if image is out: raise NotImplementedError("Cannot perform white top hat in place.") diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index 24adfce6..4147d93b 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -292,7 +292,7 @@ def star(a, dtype=np.uint8): selem[selem > 0] = 1 return selem.astype(dtype) - + def _default_selem(ndim): """ Generates a cross-shaped structuring element (connectivity=1). This is the diff --git a/skimage/morphology/tests/test_binary.py b/skimage/morphology/tests/test_binary.py index e6d80321..6ac5e8d8 100644 --- a/skimage/morphology/tests/test_binary.py +++ b/skimage/morphology/tests/test_binary.py @@ -65,10 +65,8 @@ def test_out_argument(): testing.assert_array_equal(out, func(img, strel)) def test_default_selem(): - functions = [binary.binary_erosion, binary.binary_dilation, binary.binary_opening, binary.binary_closing] - strel = selem.diamond(radius=1) image = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -83,11 +81,10 @@ def test_default_selem(): [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], np.uint8) - for function in functions: im_expected = function(image, strel) im_test = function(image) yield testing.assert_array_equal, im_expected, im_test - + if __name__ == '__main__': testing.run_module_suite() diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index d298b1eb..359a2832 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -120,11 +120,9 @@ class TestEccentricStructuringElements(): assert np.all(tophat == 0) def test_default_selem(): - functions = [grey.erosion, grey.dilation, grey.opening, grey.closing, grey.white_tophat, grey.black_tophat] - strel = selem.diamond(radius=1) image = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -139,13 +137,11 @@ def test_default_selem(): [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], np.uint8) - for function in functions: im_expected = function(image, strel) im_test = function(image) yield testing.assert_array_equal, im_expected, im_test - - + class TestDTypes(): def setUp(self): From 4c000c4d89a3a0458353b79c05f719456595c320 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 11 Jul 2014 17:01:54 -0500 Subject: [PATCH 0121/1122] Use normal deprecation warning --- skimage/_shared/utils.py | 10 ---------- skimage/exposure/exposure.py | 5 ++--- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index 40703206..612a2b63 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -19,16 +19,6 @@ class skimage_deprecation(Warning): pass -def deprecation_warning(msg, **kwargs): - """Emit deprecation warning for scikit-image. - - Unlike default deprecation warnings, this is not silenced by default. - Additional keyword arguments are passed to `warnings.warn`. - """ - warnings.simplefilter('always', skimage_deprecation) - warnings.warn(msg, category=skimage_deprecation, **kwargs) - - class deprecated(object): """Decorator to mark deprecated functions with warning. diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index ad21fa9b..3a2accbc 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -3,7 +3,6 @@ import numpy as np from skimage import img_as_float from skimage.util.dtype import dtype_range, dtype_limits -from skimage._shared.utils import deprecation_warning __all__ = ['histogram', 'cumulative_distribution', 'equalize', @@ -255,12 +254,12 @@ def rescale_intensity(image, in_range='image', out_range='dtype'): if in_range is None: in_range = 'image' msg = "`in_range` should not be set to None. Use {!r} instead." - deprecation_warning(msg.format(in_range)) + warnings.warn(msg.format(in_range)) if out_range is None: out_range = 'dtype' msg = "`out_range` should not be set to None. Use {!r} instead." - deprecation_warning(msg.format(out_range)) + warnings.warn(msg.format(out_range)) imin, imax = intensity_range(image, in_range) omin, omax = intensity_range(image, out_range, clip_negative=(imin >= 0)) From 9cd9a1372ae98a8787221e99b0339a1876e09255 Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Fri, 11 Jul 2014 15:19:33 -0700 Subject: [PATCH 0122/1122] Address reviewer comments Add newline at EOF of selem.py Add "optional" to grey.py and binary.py --- skimage/morphology/binary.py | 2 +- skimage/morphology/grey.py | 2 +- skimage/morphology/selem.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index 5b019051..4612a35d 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -21,7 +21,7 @@ def binary_erosion(image, selem=None, out=None): selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. If None, use cross-shaped structuring element (connectivity=1). - out : ndarray of bool + out : ndarray of bool, optional The array to store the result of the morphology. If None is passed, a new array will be allocated. diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 2b4a01a8..565ec9a6 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -307,7 +307,7 @@ def black_tophat(image, selem=None, out=None): selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. If None, use cross-shaped structuring element (connectivity=1). - out : ndarray + out : ndarray, optional The array to store the result of the morphology. If None is passed, a new array will be allocated. diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index 4147d93b..36ce4fe9 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -310,4 +310,5 @@ def _default_selem(ndim): are 1 and 0 otherwise. """ - return ndimage.morphology.generate_binary_structure(ndim, 1) \ No newline at end of file + return ndimage.morphology.generate_binary_structure(ndim, 1) + \ No newline at end of file From 7c53880f06d089ed9d5765459e63dc000bc977c2 Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Fri, 11 Jul 2014 15:26:31 -0700 Subject: [PATCH 0123/1122] Add gitignore for *.pyd Windows libraries --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1026f756..68bfaecb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *# *egg-info *.so +*.pyd *.bak *.c *.new From c2bbca6113e78adf7e263aaa9156e109006124ef Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Fri, 11 Jul 2014 23:32:36 -0700 Subject: [PATCH 0124/1122] Add a newline at EOF --- skimage/morphology/selem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index 36ce4fe9..b7b65d72 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -311,4 +311,4 @@ def _default_selem(ndim): """ return ndimage.morphology.generate_binary_structure(ndim, 1) - \ No newline at end of file + From 3518e65ab456ebba870a22632cad201a2dba8484 Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Fri, 11 Jul 2014 23:39:07 -0700 Subject: [PATCH 0125/1122] Add newline at EOF of selem.py --- skimage/morphology/selem.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index b7b65d72..1822c8b3 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -312,3 +312,9 @@ def _default_selem(ndim): """ return ndimage.morphology.generate_binary_structure(ndim, 1) + + + + + + From 2b91c259d632462f1e0cbb57c9df754e73d201c0 Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Sat, 12 Jul 2014 15:22:48 -0700 Subject: [PATCH 0126/1122] Add fallback decorator for 3D images We don't support images greater than 2D, so fall back on ndimage --- skimage/morphology/binary.py | 13 ++++------ skimage/morphology/grey.py | 37 ++++++++++----------------- skimage/morphology/misc.py | 28 ++++++++++++++++++++ skimage/morphology/selem.py | 7 ----- skimage/morphology/tests/test_grey.py | 24 +++++++++++++++++ 5 files changed, 71 insertions(+), 38 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index 4612a35d..69634912 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -2,8 +2,10 @@ import warnings import numpy as np from scipy import ndimage from .selem import _default_selem +from .misc import default_fallback +@default_fallback def binary_erosion(image, selem=None, out=None): """Return fast binary morphological erosion of an image. @@ -32,10 +34,6 @@ def binary_erosion(image, selem=None, out=None): """ - # Default structure element - if selem is None: - selem = _default_selem(image.ndim) - selem = (selem != 0) selem_sum = np.sum(selem) @@ -52,6 +50,7 @@ def binary_erosion(image, selem=None, out=None): return np.equal(conv, selem_sum, out=out) +@default_fallback def binary_dilation(image, selem=None, out=None): """Return fast binary morphological dilation of an image. @@ -81,10 +80,6 @@ def binary_dilation(image, selem=None, out=None): """ - # Default structure element - if selem is None: - selem = _default_selem(image.ndim) - selem = (selem != 0) if np.sum(selem) <= 255: @@ -100,6 +95,7 @@ def binary_dilation(image, selem=None, out=None): return np.not_equal(conv, 0, out=out) +@default_fallback def binary_opening(image, selem=None, out=None): """Return fast binary morphological opening of an image. @@ -133,6 +129,7 @@ def binary_opening(image, selem=None, out=None): return out +@default_fallback def binary_closing(image, selem=None, out=None): """Return fast binary morphological closing of an image. diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 565ec9a6..0d0290c4 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -2,7 +2,7 @@ import warnings from skimage import img_as_ubyte from scipy import ndimage from .selem import _default_selem - +from .misc import default_fallback from . import cmorph @@ -11,6 +11,7 @@ __all__ = ['erosion', 'dilation', 'opening', 'closing', 'white_tophat', 'black_tophat'] +@default_fallback def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): """Return greyscale morphological erosion of an image. @@ -56,9 +57,9 @@ def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): """ - # Default structure element - if selem is None: - selem = _default_selem(image.ndim) + # If image has more than 2 dimensions, use scipy.ndimage + if image.ndim > 2: + return ndimage.morphology.grey_erosion(image, footprint=selem, out=out) if image is out: raise NotImplementedError("In-place erosion not supported!") @@ -68,6 +69,7 @@ def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): shift_x=shift_x, shift_y=shift_y) +@default_fallback def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): """Return greyscale morphological dilation of an image. @@ -114,18 +116,20 @@ def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): """ - # Default structure element - if selem is None: - selem = _default_selem(image.ndim) + # If image has more than 2 dimensions, use scipy.ndimage + if image.ndim > 2: + return ndimage.morphology.grey_dilation(image, footprint=selem,out=out) if image is out: raise NotImplementedError("In-place dilation not supported!") + image = img_as_ubyte(image) selem = img_as_ubyte(selem) return cmorph._dilate(image, selem, out=out, shift_x=shift_x, shift_y=shift_y) +@default_fallback def opening(image, selem=None, out=None): """Return greyscale morphological opening of an image. @@ -169,10 +173,6 @@ def opening(image, selem=None, out=None): """ - # Default structure element - if selem is None: - selem = _default_selem(image.ndim) - h, w = selem.shape shift_x = True if (w % 2) == 0 else False shift_y = True if (h % 2) == 0 else False @@ -182,6 +182,7 @@ def opening(image, selem=None, out=None): return out +@default_fallback def closing(image, selem=None, out=None): """Return greyscale morphological closing of an image. @@ -225,10 +226,6 @@ def closing(image, selem=None, out=None): """ - # Default structure element - if selem is None: - selem = _default_selem(image.ndim) - h, w = selem.shape shift_x = True if (w % 2) == 0 else False shift_y = True if (h % 2) == 0 else False @@ -238,6 +235,7 @@ def closing(image, selem=None, out=None): return out +@default_fallback def white_tophat(image, selem=None, out=None): """Return white top hat of an image. @@ -280,10 +278,6 @@ def white_tophat(image, selem=None, out=None): """ - # Default structure element - if selem is None: - selem = _default_selem(image.ndim) - if image is out: raise NotImplementedError("Cannot perform white top hat in place.") @@ -292,6 +286,7 @@ def white_tophat(image, selem=None, out=None): return out +@default_fallback def black_tophat(image, selem=None, out=None): """Return black top hat of an image. @@ -335,10 +330,6 @@ def black_tophat(image, selem=None, out=None): """ - # Default structure element - if selem is None: - selem = _default_selem(image.ndim) - if image is out: raise NotImplementedError("Cannot perform white top hat in place.") diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index dc8290d2..f61d4e56 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -1,5 +1,33 @@ import numpy as np import scipy.ndimage as nd +from .selem import _default_selem + +skimage2ndimage = {x: 'grey_' + x + for x in ('erosion','dilation','opening','closing')} +skimage2ndimage.update({x: x + for x in ('binary_erosion','binary_dilation', + 'binary_opening','binary_closing', + 'black_tophat','white_tophat')}) + + +def default_fallback(func): + def func_out(image, selem=None, out=None, **kwargs): + # Default structure element + if selem is None: + selem = _default_selem(image.ndim) + + # If image has more than 2 dimensions, use scipy.ndimage + if image.ndim > 2: + function = getattr(nd, skimage2ndimage[func.__name__]) + try: + return function(image, footprint=selem, output=out, **kwargs) + except TypeError: + # nd.binary_* take structure instead of footprint + return function(image, structure=selem, output=out, **kwargs) + else: + return func(image, selem=selem, out=out, **kwargs) + + return func_out def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index 1822c8b3..5e0606c0 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -311,10 +311,3 @@ def _default_selem(ndim): """ return ndimage.morphology.generate_binary_structure(ndim, 1) - - - - - - - diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index 359a2832..840b9c42 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -142,6 +142,30 @@ def test_default_selem(): im_test = function(image) yield testing.assert_array_equal, im_expected, im_test +def test_3d_fallback_default_selem(): + # 3x3x3 cube inside a 7x7x7 image: + image = np.zeros((7, 7, 7), np.bool) + image[2:-2, 2:-2, 2:-2] = 1 + + opened = morphology.opening(image) + + # expect a "hyper-cross" centered in the 5x5x5: + image_expected = np.zeros((7, 7, 7), dtype=bool) + image_expected[2:5, 2:5, 2:5] = ndimage.generate_binary_structure(3, 1) + testing.assert_array_equal(opened, image_expected) + +def test_3d_fallback_default_selem(): + # 3x3x3 cube inside a 7x7x7 image: + image = np.zeros((7, 7, 7), np.bool) + image[2:-2, 2:-2, 2:-2] = 1 + + cube = np.ones((3, 3, 3),dtype=np.uint8) + + for function in [grey.closing, grey.opening]: + new_image = function(image, cube) + yield testing.assert_array_equal, new_image, image + + class TestDTypes(): def setUp(self): From ea3e65f25a4b847c66a4e645fb52831391284726 Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Sat, 12 Jul 2014 16:54:35 -0700 Subject: [PATCH 0127/1122] Add 3d-fallback tests for binary functions --- skimage/morphology/tests/test_binary.py | 24 ++++++++++++++++++++++++ skimage/morphology/tests/test_grey.py | 7 ++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/skimage/morphology/tests/test_binary.py b/skimage/morphology/tests/test_binary.py index 6ac5e8d8..c0c4c682 100644 --- a/skimage/morphology/tests/test_binary.py +++ b/skimage/morphology/tests/test_binary.py @@ -4,6 +4,7 @@ from numpy import testing from skimage import data, color from skimage.util import img_as_bool from skimage.morphology import binary, grey, selem +from scipy import ndimage lena = color.rgb2gray(data.lena()) @@ -86,5 +87,28 @@ def test_default_selem(): im_test = function(image) yield testing.assert_array_equal, im_expected, im_test +def test_3d_fallback_default_selem(): + # 3x3x3 cube inside a 7x7x7 image: + image = np.zeros((7, 7, 7), np.bool) + image[2:-2, 2:-2, 2:-2] = 1 + + opened = binary.binary_opening(image) + + # expect a "hyper-cross" centered in the 5x5x5: + image_expected = np.zeros((7, 7, 7), dtype=bool) + image_expected[2:5, 2:5, 2:5] = ndimage.generate_binary_structure(3, 1) + testing.assert_array_equal(opened, image_expected) + +def test_3d_fallback_cube_selem(): + # 3x3x3 cube inside a 7x7x7 image: + image = np.zeros((7, 7, 7), np.bool) + image[2:-2, 2:-2, 2:-2] = 1 + + cube = np.ones((3, 3, 3), dtype=np.uint8) + + for function in [binary.binary_closing, binary.binary_opening]: + new_image = function(image, cube) + yield testing.assert_array_equal, new_image, image + if __name__ == '__main__': testing.run_module_suite() diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index 840b9c42..191613c4 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -2,6 +2,7 @@ import os.path import numpy as np from numpy import testing +from scipy import ndimage import skimage from skimage import data_dir @@ -147,19 +148,19 @@ def test_3d_fallback_default_selem(): image = np.zeros((7, 7, 7), np.bool) image[2:-2, 2:-2, 2:-2] = 1 - opened = morphology.opening(image) + opened = grey.opening(image) # expect a "hyper-cross" centered in the 5x5x5: image_expected = np.zeros((7, 7, 7), dtype=bool) image_expected[2:5, 2:5, 2:5] = ndimage.generate_binary_structure(3, 1) testing.assert_array_equal(opened, image_expected) -def test_3d_fallback_default_selem(): +def test_3d_fallback_cube_selem(): # 3x3x3 cube inside a 7x7x7 image: image = np.zeros((7, 7, 7), np.bool) image[2:-2, 2:-2, 2:-2] = 1 - cube = np.ones((3, 3, 3),dtype=np.uint8) + cube = np.ones((3, 3, 3), dtype=np.uint8) for function in [grey.closing, grey.opening]: new_image = function(image, cube) From 196205956e131f073e158facd6101096c0f06ab8 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sat, 8 Feb 2014 20:05:47 -0600 Subject: [PATCH 0128/1122] Set the unused portion of the image to 0 --- skimage/exposure/_adapthist.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/skimage/exposure/_adapthist.py b/skimage/exposure/_adapthist.py index 699e9acf..aa5552f5 100644 --- a/skimage/exposure/_adapthist.py +++ b/skimage/exposure/_adapthist.py @@ -51,8 +51,7 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, Notes ----- * The algorithm relies on an image whose rows and columns are even - multiples of the number of tiles, so the extra rows and columns are left - at their original values, thus preserving the input image shape. + multiples of the number of tiles, so the extra rows and columns are to zero, thus preserving the input image shape. * For color images, the following steps are performed: - The image is converted to LAB color space - The CLAHE algorithm is run on the L channel @@ -73,14 +72,20 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, args[0] = rescale_intensity(l_chan, out_range=(0, NR_OF_GREY - 1)) new_l = _clahe(*args).astype(float) new_l = rescale_intensity(new_l, out_range=(0, 100)) - lab_img[:new_l.shape[0], :new_l.shape[1], 0] = new_l + col, row = new_l.shape + lab_img[:col, :row, 0] = new_l + lab_img[col:, :, 0] = 0 + lab_img[:, row:, 0] = 0 image = color.lab2rgb(lab_img) image = rescale_intensity(image, out_range=(0, 1)) else: image = skimage.img_as_uint(image) args[0] = rescale_intensity(image, out_range=(0, NR_OF_GREY - 1)) out = _clahe(*args) - image[:out.shape[0], :out.shape[1]] = out + col, row = out.shape + image[:col, :row] = out + image[col:, :] = 0 + image[:, row:] = 0 image = rescale_intensity(image) return image From 61fb831d31403d6725b0d786e5fb96a3c8302333 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sat, 8 Feb 2014 20:15:19 -0600 Subject: [PATCH 0129/1122] Implement a mode enum --- skimage/exposure/_adapthist.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/skimage/exposure/_adapthist.py b/skimage/exposure/_adapthist.py index aa5552f5..241b8c8e 100644 --- a/skimage/exposure/_adapthist.py +++ b/skimage/exposure/_adapthist.py @@ -26,7 +26,7 @@ NR_OF_GREY = 16384 # number of grayscale levels to use in CLAHE algorithm def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, - nbins=256): + nbins=256, mode='unchanged'): """Contrast Limited Adaptive Histogram Equalization. Parameters @@ -42,6 +42,8 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, contrast). nbins : int, optional Number of gray bins for histogram ("dynamic range"). + mode : string, one of {'unchanged', 'zero', 'trim'}, optional + How to treat any pixels falling outside of the tiles. Returns ------- @@ -72,20 +74,28 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, args[0] = rescale_intensity(l_chan, out_range=(0, NR_OF_GREY - 1)) new_l = _clahe(*args).astype(float) new_l = rescale_intensity(new_l, out_range=(0, 100)) - col, row = new_l.shape - lab_img[:col, :row, 0] = new_l - lab_img[col:, :, 0] = 0 - lab_img[:, row:, 0] = 0 + if mode == 'trim': + lab_img = new_l + else: + col, row = new_l.shape + lab_img[:col, :row, 0] = new_l + if mode == 'zero': + lab_img[col:, :, 0] = 0 + lab_img[:, row:, 0] = 0 image = color.lab2rgb(lab_img) image = rescale_intensity(image, out_range=(0, 1)) else: image = skimage.img_as_uint(image) args[0] = rescale_intensity(image, out_range=(0, NR_OF_GREY - 1)) out = _clahe(*args) - col, row = out.shape - image[:col, :row] = out - image[col:, :] = 0 - image[:, row:] = 0 + if mode == 'trim': + image = out + else: + col, row = out.shape + image[:col, :row] = out + if mode == 'zero': + image[col:, :] = 0 + image[:, row:] = 0 image = rescale_intensity(image) return image From 9fa39e564a25aa706f4968f5b02871bb57db3240 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sat, 8 Feb 2014 20:15:36 -0600 Subject: [PATCH 0130/1122] Update docstring --- skimage/exposure/_adapthist.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/skimage/exposure/_adapthist.py b/skimage/exposure/_adapthist.py index 241b8c8e..552591f4 100644 --- a/skimage/exposure/_adapthist.py +++ b/skimage/exposure/_adapthist.py @@ -43,7 +43,7 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, nbins : int, optional Number of gray bins for histogram ("dynamic range"). mode : string, one of {'unchanged', 'zero', 'trim'}, optional - How to treat any pixels falling outside of the tiles. + How to treat any pixels falling outside of the tiles. See the notes. Returns ------- @@ -53,7 +53,12 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, Notes ----- * The algorithm relies on an image whose rows and columns are even - multiples of the number of tiles, so the extra rows and columns are to zero, thus preserving the input image shape. + multiples of the number of tiles, so the extra rows and columns are not + affected by the algorithm. The handling of those outlier pixels is + determined by the "mode" paramter. If mode is 'unchanged', the values + the same as the input values. If mode is 'zero', they are set to zero. + If mode is 'trim', only the portion of the image that was equalized will + be returned. * For color images, the following steps are performed: - The image is converted to LAB color space - The CLAHE algorithm is run on the L channel From 9f7119b1c0da17af7f7e9e4f671d9fcdf66e2ea0 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sun, 16 Feb 2014 19:08:14 -0600 Subject: [PATCH 0131/1122] Change 'trim' to 'crop' --- skimage/exposure/_adapthist.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/exposure/_adapthist.py b/skimage/exposure/_adapthist.py index 552591f4..f5a6d0a1 100644 --- a/skimage/exposure/_adapthist.py +++ b/skimage/exposure/_adapthist.py @@ -26,7 +26,7 @@ NR_OF_GREY = 16384 # number of grayscale levels to use in CLAHE algorithm def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, - nbins=256, mode='unchanged'): + nbins=256, mode='ignore'): """Contrast Limited Adaptive Histogram Equalization. Parameters @@ -42,7 +42,7 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, contrast). nbins : int, optional Number of gray bins for histogram ("dynamic range"). - mode : string, one of {'unchanged', 'zero', 'trim'}, optional + mode : string, one of {'unchanged', 'zero', 'crop'}, optional How to treat any pixels falling outside of the tiles. See the notes. Returns @@ -79,7 +79,7 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, args[0] = rescale_intensity(l_chan, out_range=(0, NR_OF_GREY - 1)) new_l = _clahe(*args).astype(float) new_l = rescale_intensity(new_l, out_range=(0, 100)) - if mode == 'trim': + if mode == 'crop': lab_img = new_l else: col, row = new_l.shape @@ -93,7 +93,7 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, image = skimage.img_as_uint(image) args[0] = rescale_intensity(image, out_range=(0, NR_OF_GREY - 1)) out = _clahe(*args) - if mode == 'trim': + if mode == 'crop': image = out else: col, row = out.shape From 95a208eec5fe5d7b7978c5a5cbfb62b0a5155408 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sun, 16 Feb 2014 21:10:51 -0600 Subject: [PATCH 0132/1122] Add test for adapthist modes --- skimage/exposure/tests/test_exposure.py | 35 +++++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py index dcaf6837..fb3ba78a 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -194,6 +194,31 @@ def test_adapthist_color(): return data, adapted +def test_adapthist_modes(): + '''Test adaptist `mode` parameter values. + ''' + img = skimage.img_as_float(data.moon()) + full_scale = skimage.exposure.rescale_intensity(skimage.img_as_uint(img)) + ignore = exposure.equalize_adapthist(img.copy(), ntiles_x=9, ntiles_y=9) + zero = exposure.equalize_adapthist(img.copy(), ntiles_x=9, ntiles_y=9, + mode='zero') + crop = exposure.equalize_adapthist(img.copy(), ntiles_x=9, ntiles_y=9, + mode='crop') + assert ignore.shape == zero.shape + assert ignore.shape != crop.shape + assert_array_equal(zero[crop.shape[0]:, :], 0) + assert_array_equal(zero[:, crop.shape[1]:], 0) + + assert_almost_equal = np.testing.assert_almost_equal + full_cropped = full_scale[:crop.shape[0], :crop.shape[1]] + assert_almost_equal(peak_snr(full_scale, ignore), 56.175, 3) + assert_almost_equal(peak_snr(full_scale, zero), 94.7988, 3) + assert_almost_equal(peak_snr(full_cropped, crop), 120.4598, 3) + assert_almost_equal(norm_brightness_err(full_scale, ignore), 0.223, 3) + assert_almost_equal(norm_brightness_err(full_scale, zero), 0.01589, 3) + assert_almost_equal(norm_brightness_err(full_cropped, crop), 0.02878, 3) + + def peak_snr(img1, img2): '''Peak signal to noise ratio of two images @@ -236,11 +261,6 @@ def norm_brightness_err(img1, img2): return nbe -if __name__ == '__main__': - from numpy import testing - testing.run_module_suite() - - # Test Gamma Correction # ===================== @@ -409,3 +429,8 @@ def test_adjust_inv_sigmoid_cutoff_half(): def test_negative(): image = np.arange(-10, 245, 4).reshape(8, 8).astype(np.double) assert_raises(ValueError, exposure.adjust_gamma, image) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() From 909f23baab96551af9614f6f21c30370f9bd60ba Mon Sep 17 00:00:00 2001 From: blink1073 Date: Mon, 17 Feb 2014 16:11:43 -0600 Subject: [PATCH 0133/1122] Switch to HSV, refactor equalize_adapthist, fix #757, cleanup docstring --- skimage/exposure/_adapthist.py | 77 ++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/skimage/exposure/_adapthist.py b/skimage/exposure/_adapthist.py index f5a6d0a1..0887224f 100644 --- a/skimage/exposure/_adapthist.py +++ b/skimage/exposure/_adapthist.py @@ -42,7 +42,7 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, contrast). nbins : int, optional Number of gray bins for histogram ("dynamic range"). - mode : string, one of {'unchanged', 'zero', 'crop'}, optional + mode : {'unchanged', 'zero', 'crop'}, optional How to treat any pixels falling outside of the tiles. See the notes. Returns @@ -55,13 +55,13 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, * The algorithm relies on an image whose rows and columns are even multiples of the number of tiles, so the extra rows and columns are not affected by the algorithm. The handling of those outlier pixels is - determined by the "mode" paramter. If mode is 'unchanged', the values + determined by the `mode` parameter. If mode is 'unchanged', the values the same as the input values. If mode is 'zero', they are set to zero. If mode is 'trim', only the portion of the image that was equalized will be returned. * For color images, the following steps are performed: - - The image is converted to LAB color space - - The CLAHE algorithm is run on the L channel + - The image is converted to HSV color space + - The CLAHE algorithm is run on the V (Value) channel - The image is converted back to RGB space and returned * For RGBA images, the original alpha channel is removed. @@ -70,38 +70,36 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, .. [1] http://tog.acm.org/resources/GraphicsGems/gems.html#gemsvi .. [2] https://en.wikipedia.org/wiki/CLAHE#CLAHE """ - args = [None, ntiles_x, ntiles_y, clip_limit * nbins, nbins] - if image.ndim > 2: - lab_img = color.rgb2lab(skimage.img_as_float(image)) - l_chan = lab_img[:, :, 0] - l_chan /= np.max(np.abs(l_chan)) - l_chan = skimage.img_as_uint(l_chan) - args[0] = rescale_intensity(l_chan, out_range=(0, NR_OF_GREY - 1)) - new_l = _clahe(*args).astype(float) - new_l = rescale_intensity(new_l, out_range=(0, 100)) - if mode == 'crop': - lab_img = new_l - else: - col, row = new_l.shape - lab_img[:col, :row, 0] = new_l - if mode == 'zero': - lab_img[col:, :, 0] = 0 - lab_img[:, row:, 0] = 0 - image = color.lab2rgb(lab_img) - image = rescale_intensity(image, out_range=(0, 1)) - else: - image = skimage.img_as_uint(image) - args[0] = rescale_intensity(image, out_range=(0, NR_OF_GREY - 1)) - out = _clahe(*args) - if mode == 'crop': - image = out - else: - col, row = out.shape - image[:col, :row] = out - if mode == 'zero': - image[col:, :] = 0 - image[:, row:] = 0 + ndim = image.ndim + if ndim == 3: + if image.shape[2] == 4: + image = image[:, :, :3] + image = skimage.img_as_float(image) image = rescale_intensity(image) + hsv_img = color.rgb2hsv(image) + image = hsv_img[:, :, 2].copy() + image = skimage.img_as_uint(image) + image = rescale_intensity(image, out_range=(0, NR_OF_GREY - 1)) + out = _clahe(image, ntiles_x, ntiles_y, clip_limit * nbins, nbins) + if mode == 'crop': + image = image[:out.shape[0], :out.shape[1]] + if ndim == 3: + hsv_img = hsv_img[:out.shape[0], :out.shape[1], :] + image[:out.shape[0], :out.shape[1]] = out + image = skimage.img_as_float(image) + if ndim == 3: + hsv_img[:, :, 2] = rescale_intensity(image) + image = color.hsv2rgb(hsv_img) + else: + image = rescale_intensity(image) + if mode == 'zero': + mask = np.zeros(image.shape) + if ndim == 3: + for i in range(3): + mask[:out.shape[0], :out.shape[1], i] = 1 + else: + mask[:out.shape[0], :out.shape[1]] = 1 + image[mask == 0] = 0 return image @@ -134,8 +132,8 @@ def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): """ ntiles_x = min(ntiles_x, MAX_REG_X) ntiles_y = min(ntiles_y, MAX_REG_Y) - ntiles_y = max(ntiles_x, 2) - ntiles_x = max(ntiles_y, 2) + ntiles_y = max(ntiles_y, 2) + ntiles_x = max(ntiles_x, 2) if clip_limit == 1.0: return image # is OK, immediately returns original image. @@ -144,6 +142,11 @@ def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): y_res = image.shape[0] - image.shape[0] % ntiles_y x_res = image.shape[1] - image.shape[1] % ntiles_x + # make the tile size divisible by 2 + while y_res % (2 * ntiles_y): + y_res -= 1 + while x_res % (2 * ntiles_x): + x_res -= 1 image = image[: y_res, : x_res] x_size = image.shape[1] // ntiles_x # Actual size of contextual regions From ba010a3191ad03ec65d97e32f190d699c165255f Mon Sep 17 00:00:00 2001 From: blink1073 Date: Mon, 17 Feb 2014 16:13:52 -0600 Subject: [PATCH 0134/1122] Update adapthist tests, add test for alpha channel and rgb mode handling. --- skimage/exposure/tests/test_exposure.py | 86 +++++++++++++++++++------ 1 file changed, 67 insertions(+), 19 deletions(-) diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py index fb3ba78a..01d10219 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -11,6 +11,7 @@ from skimage.exposure.exposure import intensity_range from skimage.color import rgb2gray from skimage.util.dtype import dtype_range +import matplotlib.pyplot as plt # Test histogram equalization # =========================== @@ -147,13 +148,13 @@ def test_adapthist_scalar(): ''' img = skimage.img_as_ubyte(data.moon()) adapted = exposure.equalize_adapthist(img, clip_limit=0.02) - assert adapted.min() == 0 - assert adapted.max() == (1 << 16) - 1 + assert adapted.min() == 0.0 + assert adapted.max() == 1.0 assert img.shape == adapted.shape - full_scale = skimage.exposure.rescale_intensity(skimage.img_as_uint(img)) + full_scale = skimage.exposure.rescale_intensity(skimage.img_as_float(img)) assert_almost_equal = np.testing.assert_almost_equal - assert_almost_equal(peak_snr(full_scale, adapted), 101.231, 3) + assert_almost_equal(peak_snr(full_scale, adapted), 101.2295, 3) assert_almost_equal(norm_brightness_err(full_scale, adapted), 0.041, 3) return img, adapted @@ -169,8 +170,8 @@ def test_adapthist_grayscale(): nbins=128) assert_almost_equal = np.testing.assert_almost_equal assert img.shape == adapted.shape - assert_almost_equal(peak_snr(img, adapted), 97.531, 3) - assert_almost_equal(norm_brightness_err(img, adapted), 0.0313, 3) + assert_almost_equal(peak_snr(img, adapted), 101.4070, 3) + assert_almost_equal(norm_brightness_err(img, adapted), 0.03856, 3) return data, adapted @@ -183,19 +184,36 @@ def test_adapthist_color(): hist, bin_centers = exposure.histogram(img) assert len(w) > 0 adapted = exposure.equalize_adapthist(img, clip_limit=0.01) + assert_almost_equal = np.testing.assert_almost_equal assert adapted.min() == 0 assert adapted.max() == 1.0 assert img.shape == adapted.shape full_scale = skimage.exposure.rescale_intensity(img) - assert_almost_equal(peak_snr(full_scale, adapted), 102.940, 3) + assert_almost_equal(peak_snr(full_scale, adapted), 106.865, 3) assert_almost_equal(norm_brightness_err(full_scale, adapted), - 0.0110, 3) + 0.0509, 3) return data, adapted -def test_adapthist_modes(): - '''Test adaptist `mode` parameter values. +def test_adapthist_alpha(): + '''Test an RGBA color image + ''' + img = skimage.img_as_float(data.lena()) + alpha = np.ones((img.shape[0], img.shape[1]), dtype=float) + img = np.dstack((img, alpha)) + adapted = exposure.equalize_adapthist(img) + assert adapted.shape != img.shape + img = img[:, :, :3] + full_scale = skimage.exposure.rescale_intensity(img) + assert img.shape == adapted.shape + assert_almost_equal = np.testing.assert_almost_equal + assert_almost_equal(peak_snr(full_scale, adapted), 106.862, 3) + assert_almost_equal(norm_brightness_err(full_scale, adapted), 0.0509, 3) + + +def test_adapthist_modes_scalar(): + '''Test adaptist `mode` parameter values for ndim==2 image. ''' img = skimage.img_as_float(data.moon()) full_scale = skimage.exposure.rescale_intensity(skimage.img_as_uint(img)) @@ -211,12 +229,40 @@ def test_adapthist_modes(): assert_almost_equal = np.testing.assert_almost_equal full_cropped = full_scale[:crop.shape[0], :crop.shape[1]] - assert_almost_equal(peak_snr(full_scale, ignore), 56.175, 3) - assert_almost_equal(peak_snr(full_scale, zero), 94.7988, 3) - assert_almost_equal(peak_snr(full_cropped, crop), 120.4598, 3) - assert_almost_equal(norm_brightness_err(full_scale, ignore), 0.223, 3) - assert_almost_equal(norm_brightness_err(full_scale, zero), 0.01589, 3) - assert_almost_equal(norm_brightness_err(full_cropped, crop), 0.02878, 3) + zero_cropped = zero[:crop.shape[0], :crop.shape[1]] + ignore_cropped = ignore[:crop.shape[0], :crop.shape[1]] + + for crop_img in [ignore_cropped, zero_cropped, crop]: + assert_almost_equal(peak_snr(full_cropped, crop_img), 120.456, 3) + assert_almost_equal(norm_brightness_err(full_cropped, crop_img), + 0.4398, 3) + + +def test_adapthist_modes_rgb(): + '''Test adaptist `mode` parameter values for rgb image. + ''' + img = skimage.img_as_float(data.lena()) + full_scale = skimage.exposure.rescale_intensity(skimage.img_as_uint(img)) + ignore = exposure.equalize_adapthist(img.copy(), ntiles_x=9, ntiles_y=10) + zero = exposure.equalize_adapthist(img.copy(), ntiles_x=9, ntiles_y=10, + mode='zero') + crop = exposure.equalize_adapthist(img.copy(), ntiles_x=9, ntiles_y=10, + mode='crop') + assert ignore.shape == zero.shape + assert ignore.shape != crop.shape + + assert_array_equal(zero[crop.shape[0]:, :, :], 0) + assert_array_equal(zero[:, crop.shape[1]:, :], 0) + + assert_almost_equal = np.testing.assert_almost_equal + full_cropped = full_scale[:crop.shape[0], :crop.shape[1], :] + zero_cropped = zero[:crop.shape[0], :crop.shape[1], :] + ignore_cropped = ignore[:crop.shape[0], :crop.shape[1], :] + + for crop_img in [ignore_cropped, zero_cropped, crop]: + assert np.floor(peak_snr(full_cropped, crop_img)) == 106 + assert_almost_equal(norm_brightness_err(full_cropped, crop_img), + 0.0517, 3) def peak_snr(img1, img2): @@ -260,6 +306,8 @@ def norm_brightness_err(img1, img2): nbe = ambe / dtype_range[img1.dtype.type][1] return nbe +test_adapthist_modes_rgb() + # Test Gamma Correction # ===================== @@ -431,6 +479,6 @@ def test_negative(): assert_raises(ValueError, exposure.adjust_gamma, image) -if __name__ == '__main__': - from numpy import testing - testing.run_module_suite() +#if __name__ == '__main__': +# from numpy import testing +# testing.run_module_suite() From 2bdfcc7d5e4384406632df260386d5f1f57afbd9 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Mon, 17 Feb 2014 16:34:02 -0600 Subject: [PATCH 0135/1122] Fix failing tests and turn module level testing back ony --- skimage/exposure/tests/test_exposure.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py index 01d10219..3ee2a492 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -170,8 +170,8 @@ def test_adapthist_grayscale(): nbins=128) assert_almost_equal = np.testing.assert_almost_equal assert img.shape == adapted.shape - assert_almost_equal(peak_snr(img, adapted), 101.4070, 3) - assert_almost_equal(norm_brightness_err(img, adapted), 0.03856, 3) + assert_almost_equal(peak_snr(img, adapted), 105.669, 3) + assert_almost_equal(norm_brightness_err(img, adapted), 0.02470, 3) return data, adapted @@ -190,9 +190,9 @@ def test_adapthist_color(): assert adapted.max() == 1.0 assert img.shape == adapted.shape full_scale = skimage.exposure.rescale_intensity(img) - assert_almost_equal(peak_snr(full_scale, adapted), 106.865, 3) + assert_almost_equal(peak_snr(full_scale, adapted), 105.50517, 3) assert_almost_equal(norm_brightness_err(full_scale, adapted), - 0.0509, 3) + 0.0544, 3) return data, adapted @@ -208,8 +208,8 @@ def test_adapthist_alpha(): full_scale = skimage.exposure.rescale_intensity(img) assert img.shape == adapted.shape assert_almost_equal = np.testing.assert_almost_equal - assert_almost_equal(peak_snr(full_scale, adapted), 106.862, 3) - assert_almost_equal(norm_brightness_err(full_scale, adapted), 0.0509, 3) + assert_almost_equal(peak_snr(full_scale, adapted), 105.50198, 3) + assert_almost_equal(norm_brightness_err(full_scale, adapted), 0.0544, 3) def test_adapthist_modes_scalar(): @@ -306,7 +306,6 @@ def norm_brightness_err(img1, img2): nbe = ambe / dtype_range[img1.dtype.type][1] return nbe -test_adapthist_modes_rgb() # Test Gamma Correction @@ -479,6 +478,6 @@ def test_negative(): assert_raises(ValueError, exposure.adjust_gamma, image) -#if __name__ == '__main__': -# from numpy import testing -# testing.run_module_suite() +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() From cc68b0c392defab55f6a6252bf9bc290eff73554 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Fri, 11 Jul 2014 13:28:00 -0500 Subject: [PATCH 0136/1122] Start removing optional edge pixel handling --- skimage/exposure/_adapthist.py | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/skimage/exposure/_adapthist.py b/skimage/exposure/_adapthist.py index 0887224f..d583424d 100644 --- a/skimage/exposure/_adapthist.py +++ b/skimage/exposure/_adapthist.py @@ -42,8 +42,6 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, contrast). nbins : int, optional Number of gray bins for histogram ("dynamic range"). - mode : {'unchanged', 'zero', 'crop'}, optional - How to treat any pixels falling outside of the tiles. See the notes. Returns ------- @@ -52,18 +50,14 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, Notes ----- - * The algorithm relies on an image whose rows and columns are even - multiples of the number of tiles, so the extra rows and columns are not - affected by the algorithm. The handling of those outlier pixels is - determined by the `mode` parameter. If mode is 'unchanged', the values - the same as the input values. If mode is 'zero', they are set to zero. - If mode is 'trim', only the portion of the image that was equalized will - be returned. * For color images, the following steps are performed: - The image is converted to HSV color space - The CLAHE algorithm is run on the V (Value) channel - The image is converted back to RGB space and returned * For RGBA images, the original alpha channel is removed. + * The CLAHE algorithm relies on image blocks of equal size. This results + in extra border pixels that are not handled. Extra blocks are created + around the border to handle these pixels. References ---------- @@ -81,10 +75,6 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, image = skimage.img_as_uint(image) image = rescale_intensity(image, out_range=(0, NR_OF_GREY - 1)) out = _clahe(image, ntiles_x, ntiles_y, clip_limit * nbins, nbins) - if mode == 'crop': - image = image[:out.shape[0], :out.shape[1]] - if ndim == 3: - hsv_img = hsv_img[:out.shape[0], :out.shape[1], :] image[:out.shape[0], :out.shape[1]] = out image = skimage.img_as_float(image) if ndim == 3: @@ -92,14 +82,6 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, image = color.hsv2rgb(hsv_img) else: image = rescale_intensity(image) - if mode == 'zero': - mask = np.zeros(image.shape) - if ndim == 3: - for i in range(3): - mask[:out.shape[0], :out.shape[1], i] = 1 - else: - mask[:out.shape[0], :out.shape[1]] = 1 - image[mask == 0] = 0 return image From 5cafbd95e681bbb555ec4d6b1d7097af4eb3400f Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 11 Jul 2014 16:30:18 -0500 Subject: [PATCH 0137/1122] Add tests for intensity_range and rename parameter --- skimage/exposure/exposure.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index 3a2accbc..1b42ecf8 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -3,6 +3,7 @@ import numpy as np from skimage import img_as_float from skimage.util.dtype import dtype_range, dtype_limits +from skimage._shared.utils import deprecation_warning __all__ = ['histogram', 'cumulative_distribution', 'equalize', From a924e55b5276dfd37d30bb505471f8fa468dc725 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sat, 12 Jul 2014 09:40:30 -0500 Subject: [PATCH 0138/1122] Fix handling of border pixels and update tests --- skimage/exposure/_adapthist.py | 42 ++++++++++++------ skimage/exposure/tests/test_exposure.py | 59 ++----------------------- 2 files changed, 33 insertions(+), 68 deletions(-) diff --git a/skimage/exposure/_adapthist.py b/skimage/exposure/_adapthist.py index d583424d..c89ba35b 100644 --- a/skimage/exposure/_adapthist.py +++ b/skimage/exposure/_adapthist.py @@ -55,9 +55,10 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, - The CLAHE algorithm is run on the V (Value) channel - The image is converted back to RGB space and returned * For RGBA images, the original alpha channel is removed. - * The CLAHE algorithm relies on image blocks of equal size. This results - in extra border pixels that are not handled. Extra blocks are created - around the border to handle these pixels. + * The CLAHE algorithm relies on image blocks of equal size. This may + result in extra border pixels that would not be handled. In that case, + we pad the image with a repeat of the border pixels, apply the + algorithm, and then trim the image to original size. References ---------- @@ -120,19 +121,36 @@ def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): if clip_limit == 1.0: return image # is OK, immediately returns original image. - map_array = np.zeros((ntiles_y, ntiles_x, nbins), dtype=int) - y_res = image.shape[0] - image.shape[0] % ntiles_y x_res = image.shape[1] - image.shape[1] % ntiles_x + # make the tile size divisible by 2 while y_res % (2 * ntiles_y): y_res -= 1 while x_res % (2 * ntiles_x): x_res -= 1 - image = image[: y_res, : x_res] - x_size = image.shape[1] // ntiles_x # Actual size of contextual regions - y_size = image.shape[0] // ntiles_y + orig_shape = image.shape + x_size = x_res // ntiles_x # Actual size of contextual regions + y_size = y_res // ntiles_y + + if y_res != image.shape[0]: + ntiles_y += 1 + if x_res != image.shape[1]: + ntiles_x += 1 + if y_res != image.shape[1] or x_res != image.shape[0]: + hgt = y_size * ntiles_y - image.shape[0] + wid = x_size * ntiles_x - image.shape[1] + image = np.vstack((image, image[-hgt:, :])) + image = np.hstack((image, image[:, -wid:])) + y_res, x_res = image.shape + + bin_size = 1 + NR_OF_GREY / nbins + aLUT = np.arange(NR_OF_GREY) + aLUT //= bin_size + img_blocks = view_as_blocks(image, (y_size, x_size)) + + map_array = np.zeros((ntiles_y, ntiles_x, nbins), dtype=int) n_pixels = x_size * y_size if clip_limit > 0.0: # Calculate actual cliplimit @@ -142,11 +160,6 @@ def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): else: clip_limit = NR_OF_GREY # Large value, do not clip (AHE) - bin_size = 1 + NR_OF_GREY / nbins - aLUT = np.arange(NR_OF_GREY) - aLUT //= bin_size - img_blocks = view_as_blocks(image, (y_size, x_size)) - # Calculate greylevel mappings for each contextual region for y in range(ntiles_y): for x in range(ntiles_x): @@ -203,6 +216,9 @@ def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): ystart += ystep + if image.shape != orig_shape: + image = image[:orig_shape[0], :orig_shape[1]] + return image diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py index 3ee2a492..bb584f1a 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -16,6 +16,8 @@ import matplotlib.pyplot as plt # Test histogram equalization # =========================== +np.random.seed(0) + # squeeze image intensities to lower image contrast test_img = skimage.img_as_float(data.camera()) test_img = exposure.rescale_intensity(test_img / 5. + 100) @@ -170,8 +172,8 @@ def test_adapthist_grayscale(): nbins=128) assert_almost_equal = np.testing.assert_almost_equal assert img.shape == adapted.shape - assert_almost_equal(peak_snr(img, adapted), 105.669, 3) - assert_almost_equal(norm_brightness_err(img, adapted), 0.02470, 3) + assert_almost_equal(peak_snr(img, adapted), 104.3168, 3) + assert_almost_equal(norm_brightness_err(img, adapted), 0.0265, 3) return data, adapted @@ -212,59 +214,6 @@ def test_adapthist_alpha(): assert_almost_equal(norm_brightness_err(full_scale, adapted), 0.0544, 3) -def test_adapthist_modes_scalar(): - '''Test adaptist `mode` parameter values for ndim==2 image. - ''' - img = skimage.img_as_float(data.moon()) - full_scale = skimage.exposure.rescale_intensity(skimage.img_as_uint(img)) - ignore = exposure.equalize_adapthist(img.copy(), ntiles_x=9, ntiles_y=9) - zero = exposure.equalize_adapthist(img.copy(), ntiles_x=9, ntiles_y=9, - mode='zero') - crop = exposure.equalize_adapthist(img.copy(), ntiles_x=9, ntiles_y=9, - mode='crop') - assert ignore.shape == zero.shape - assert ignore.shape != crop.shape - assert_array_equal(zero[crop.shape[0]:, :], 0) - assert_array_equal(zero[:, crop.shape[1]:], 0) - - assert_almost_equal = np.testing.assert_almost_equal - full_cropped = full_scale[:crop.shape[0], :crop.shape[1]] - zero_cropped = zero[:crop.shape[0], :crop.shape[1]] - ignore_cropped = ignore[:crop.shape[0], :crop.shape[1]] - - for crop_img in [ignore_cropped, zero_cropped, crop]: - assert_almost_equal(peak_snr(full_cropped, crop_img), 120.456, 3) - assert_almost_equal(norm_brightness_err(full_cropped, crop_img), - 0.4398, 3) - - -def test_adapthist_modes_rgb(): - '''Test adaptist `mode` parameter values for rgb image. - ''' - img = skimage.img_as_float(data.lena()) - full_scale = skimage.exposure.rescale_intensity(skimage.img_as_uint(img)) - ignore = exposure.equalize_adapthist(img.copy(), ntiles_x=9, ntiles_y=10) - zero = exposure.equalize_adapthist(img.copy(), ntiles_x=9, ntiles_y=10, - mode='zero') - crop = exposure.equalize_adapthist(img.copy(), ntiles_x=9, ntiles_y=10, - mode='crop') - assert ignore.shape == zero.shape - assert ignore.shape != crop.shape - - assert_array_equal(zero[crop.shape[0]:, :, :], 0) - assert_array_equal(zero[:, crop.shape[1]:, :], 0) - - assert_almost_equal = np.testing.assert_almost_equal - full_cropped = full_scale[:crop.shape[0], :crop.shape[1], :] - zero_cropped = zero[:crop.shape[0], :crop.shape[1], :] - ignore_cropped = ignore[:crop.shape[0], :crop.shape[1], :] - - for crop_img in [ignore_cropped, zero_cropped, crop]: - assert np.floor(peak_snr(full_cropped, crop_img)) == 106 - assert_almost_equal(norm_brightness_err(full_cropped, crop_img), - 0.0517, 3) - - def peak_snr(img1, img2): '''Peak signal to noise ratio of two images From 0392299fae08d05f2d77b10d6d12db748477bde5 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sat, 12 Jul 2014 18:54:50 -0500 Subject: [PATCH 0139/1122] Refactor to use np.pad, naming changes, update test. --- skimage/exposure/_adapthist.py | 65 ++++++++++++------------- skimage/exposure/tests/test_exposure.py | 2 +- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/skimage/exposure/_adapthist.py b/skimage/exposure/_adapthist.py index c89ba35b..22b36542 100644 --- a/skimage/exposure/_adapthist.py +++ b/skimage/exposure/_adapthist.py @@ -26,7 +26,7 @@ NR_OF_GREY = 16384 # number of grayscale levels to use in CLAHE algorithm def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, - nbins=256, mode='ignore'): + nbins=256): """Contrast Limited Adaptive Histogram Equalization. Parameters @@ -121,40 +121,39 @@ def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): if clip_limit == 1.0: return image # is OK, immediately returns original image. - y_res = image.shape[0] - image.shape[0] % ntiles_y - x_res = image.shape[1] - image.shape[1] % ntiles_x + h_inner = image.shape[0] - image.shape[0] % ntiles_y + w_inner = image.shape[1] - image.shape[1] % ntiles_x # make the tile size divisible by 2 - while y_res % (2 * ntiles_y): - y_res -= 1 - while x_res % (2 * ntiles_x): - x_res -= 1 + while h_inner % (2 * ntiles_y): + h_inner -= 1 + while w_inner % (2 * ntiles_x): + w_inner -= 1 orig_shape = image.shape - x_size = x_res // ntiles_x # Actual size of contextual regions - y_size = y_res // ntiles_y + width = w_inner // ntiles_x # Actual size of contextual regions + height = h_inner // ntiles_y - if y_res != image.shape[0]: + if h_inner != image.shape[0]: ntiles_y += 1 - if x_res != image.shape[1]: + if w_inner != image.shape[1]: ntiles_x += 1 - if y_res != image.shape[1] or x_res != image.shape[0]: - hgt = y_size * ntiles_y - image.shape[0] - wid = x_size * ntiles_x - image.shape[1] - image = np.vstack((image, image[-hgt:, :])) - image = np.hstack((image, image[:, -wid:])) - y_res, x_res = image.shape + if h_inner != image.shape[1] or w_inner != image.shape[0]: + h_pad = height * ntiles_y - image.shape[0] + w_pad = width * ntiles_x - image.shape[1] + image = np.pad(image, ((0, h_pad), (0, w_pad)), 'reflect') + h_inner, w_inner = image.shape bin_size = 1 + NR_OF_GREY / nbins - aLUT = np.arange(NR_OF_GREY) - aLUT //= bin_size - img_blocks = view_as_blocks(image, (y_size, x_size)) + lut = np.arange(NR_OF_GREY) + lut //= bin_size + img_blocks = view_as_blocks(image, (height, width)) map_array = np.zeros((ntiles_y, ntiles_x, nbins), dtype=int) - n_pixels = x_size * y_size + n_pixels = width * height if clip_limit > 0.0: # Calculate actual cliplimit - clip_limit = int(clip_limit * (x_size * y_size) / nbins) + clip_limit = int(clip_limit * (width * height) / nbins) if clip_limit < 1: clip_limit = 1 else: @@ -164,7 +163,7 @@ def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): for y in range(ntiles_y): for x in range(ntiles_x): sub_img = img_blocks[y, x] - hist = aLUT[sub_img.ravel()] + hist = lut[sub_img.ravel()] hist = np.bincount(hist) hist = np.append(hist, np.zeros(nbins - hist.size, dtype=int)) hist = clip_histogram(hist, clip_limit) @@ -176,29 +175,29 @@ def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): for y in range(ntiles_y + 1): xstart = 0 if y == 0: # special case: top row - ystep = y_size / 2.0 + ystep = height / 2.0 yU = 0 yB = 0 elif y == ntiles_y: # special case: bottom row - ystep = y_size / 2.0 + ystep = height / 2.0 yU = ntiles_y - 1 yB = yU else: # default values - ystep = y_size + ystep = height yU = y - 1 yB = yB + 1 for x in range(ntiles_x + 1): if x == 0: # special case: left column - xstep = x_size / 2.0 + xstep = width / 2.0 xL = 0 xR = 0 elif x == ntiles_x: # special case: right column - xstep = x_size / 2.0 + xstep = width / 2.0 xL = ntiles_x - 1 xR = xL else: # default values - xstep = x_size + xstep = width xL = x - 1 xR = xL + 1 @@ -210,7 +209,7 @@ def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): xslice = np.arange(xstart, xstart + xstep) yslice = np.arange(ystart, ystart + ystep) interpolate(image, xslice, yslice, - mapLU, mapRU, mapLB, mapRB, aLUT) + mapLU, mapRU, mapLB, mapRB, lut) xstart += xstep # set pointer on next matrix */ @@ -305,7 +304,7 @@ def map_histogram(hist, min_val, max_val, n_pixels): def interpolate(image, xslice, yslice, - mapLU, mapRU, mapLB, mapRB, aLUT): + mapLU, mapRU, mapLB, mapRB, lut): """Find the new grayscale level for a region using bilinear interpolation. Parameters @@ -316,7 +315,7 @@ def interpolate(image, xslice, yslice, Indices of the region. map* : ndarray Mappings of greylevels from histograms. - aLUT : ndarray + lut : ndarray Maps grayscale levels in image to histogram levels. Returns @@ -338,7 +337,7 @@ def interpolate(image, xslice, yslice, view = image[int(yslice[0]):int(yslice[-1] + 1), int(xslice[0]):int(xslice[-1] + 1)] - im_slice = aLUT[view] + im_slice = lut[view] new = ((y_inv_coef * (x_inv_coef * mapLU[im_slice] + x_coef * mapRU[im_slice]) + y_coef * (x_inv_coef * mapLB[im_slice] diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py index bb584f1a..c793c2c8 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -172,7 +172,7 @@ def test_adapthist_grayscale(): nbins=128) assert_almost_equal = np.testing.assert_almost_equal assert img.shape == adapted.shape - assert_almost_equal(peak_snr(img, adapted), 104.3168, 3) + assert_almost_equal(peak_snr(img, adapted), 104.307, 3) assert_almost_equal(norm_brightness_err(img, adapted), 0.0265, 3) return data, adapted From 70a3760bca2f1e8f724f90f3e3703bb5733b9763 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sat, 12 Jul 2014 19:43:57 -0500 Subject: [PATCH 0140/1122] Remove unused deprecation import. --- skimage/exposure/exposure.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index 1b42ecf8..3a2accbc 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -3,7 +3,6 @@ import numpy as np from skimage import img_as_float from skimage.util.dtype import dtype_range, dtype_limits -from skimage._shared.utils import deprecation_warning __all__ = ['histogram', 'cumulative_distribution', 'equalize', From f1a8132fbfadc8621f81423e238dc50f42027ff6 Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Sun, 13 Jul 2014 11:28:27 -0700 Subject: [PATCH 0141/1122] Add doctrings and comments in response to reviewers --- skimage/morphology/binary.py | 17 +++++++++++++++- skimage/morphology/grey.py | 35 ++++++++++++++++++++++----------- skimage/morphology/misc.py | 38 +++++++++++++++++++++++++++++++++++- 3 files changed, 77 insertions(+), 13 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index 69634912..5324b0f5 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -1,10 +1,13 @@ import warnings import numpy as np from scipy import ndimage -from .selem import _default_selem from .misc import default_fallback +# Our functions only work in 2D, so for 3D or higher input we should fall back +# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring +# element of the appropriate dimension for each of these functions. +# The `default_callback` provides all these. @default_fallback def binary_erosion(image, selem=None, out=None): """Return fast binary morphological erosion of an image. @@ -50,6 +53,10 @@ def binary_erosion(image, selem=None, out=None): return np.equal(conv, selem_sum, out=out) +# Our functions only work in 2D, so for 3D or higher input we should fall back +# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring +# element of the appropriate dimension for each of these functions. +# The `default_callback` provides all these. @default_fallback def binary_dilation(image, selem=None, out=None): """Return fast binary morphological dilation of an image. @@ -95,6 +102,10 @@ def binary_dilation(image, selem=None, out=None): return np.not_equal(conv, 0, out=out) +# Our functions only work in 2D, so for 3D or higher input we should fall back +# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring +# element of the appropriate dimension for each of these functions. +# The `default_callback` provides all these. @default_fallback def binary_opening(image, selem=None, out=None): """Return fast binary morphological opening of an image. @@ -129,6 +140,10 @@ def binary_opening(image, selem=None, out=None): return out +# Our functions only work in 2D, so for 3D or higher input we should fall back +# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring +# element of the appropriate dimension for each of these functions. +# The `default_callback` provides all these. @default_fallback def binary_closing(image, selem=None, out=None): """Return fast binary morphological closing of an image. diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 0d0290c4..e09872cb 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -1,7 +1,5 @@ import warnings from skimage import img_as_ubyte -from scipy import ndimage -from .selem import _default_selem from .misc import default_fallback from . import cmorph @@ -10,7 +8,10 @@ from . import cmorph __all__ = ['erosion', 'dilation', 'opening', 'closing', 'white_tophat', 'black_tophat'] - +# Our functions only work in 2D, so for 3D or higher input we should fall back +# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring +# element of the appropriate dimension for each of these functions. +# The `default_callback` provides all these. @default_fallback def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): """Return greyscale morphological erosion of an image. @@ -57,10 +58,6 @@ def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): """ - # If image has more than 2 dimensions, use scipy.ndimage - if image.ndim > 2: - return ndimage.morphology.grey_erosion(image, footprint=selem, out=out) - if image is out: raise NotImplementedError("In-place erosion not supported!") image = img_as_ubyte(image) @@ -69,6 +66,10 @@ def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): shift_x=shift_x, shift_y=shift_y) +# Our functions only work in 2D, so for 3D or higher input we should fall back +# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring +# element of the appropriate dimension for each of these functions. +# The `default_callback` provides all these. @default_fallback def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): """Return greyscale morphological dilation of an image. @@ -116,10 +117,6 @@ def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): """ - # If image has more than 2 dimensions, use scipy.ndimage - if image.ndim > 2: - return ndimage.morphology.grey_dilation(image, footprint=selem,out=out) - if image is out: raise NotImplementedError("In-place dilation not supported!") @@ -129,6 +126,10 @@ def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): shift_x=shift_x, shift_y=shift_y) +# Our functions only work in 2D, so for 3D or higher input we should fall back +# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring +# element of the appropriate dimension for each of these functions. +# The `default_callback` provides all these. @default_fallback def opening(image, selem=None, out=None): """Return greyscale morphological opening of an image. @@ -182,6 +183,10 @@ def opening(image, selem=None, out=None): return out +# Our functions only work in 2D, so for 3D or higher input we should fall back +# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring +# element of the appropriate dimension for each of these functions. +# The `default_callback` provides all these. @default_fallback def closing(image, selem=None, out=None): """Return greyscale morphological closing of an image. @@ -235,6 +240,10 @@ def closing(image, selem=None, out=None): return out +# Our functions only work in 2D, so for 3D or higher input we should fall back +# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring +# element of the appropriate dimension for each of these functions. +# The `default_callback` provides all these. @default_fallback def white_tophat(image, selem=None, out=None): """Return white top hat of an image. @@ -286,6 +295,10 @@ def white_tophat(image, selem=None, out=None): return out +# Our functions only work in 2D, so for 3D or higher input we should fall back +# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring +# element of the appropriate dimension for each of these functions. +# The `default_callback` provides all these. @default_fallback def black_tophat(image, selem=None, out=None): """Return black top hat of an image. diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index f61d4e56..db922fd7 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -2,6 +2,8 @@ import numpy as np import scipy.ndimage as nd from .selem import _default_selem +# Our function names don't exactly correspond to ndimages. +# These dictionaries translate from our names to scipy's. skimage2ndimage = {x: 'grey_' + x for x in ('erosion','dilation','opening','closing')} skimage2ndimage.update({x: x @@ -9,9 +11,43 @@ skimage2ndimage.update({x: x 'binary_opening','binary_closing', 'black_tophat','white_tophat')}) - def default_fallback(func): + """Decorator to fall back on ndimage for images with more than 2 dimensions + + Parameters + ---------- + func : function + A morphology function such as erosion, dilation, opening, closing, + white_tophat, or black_tophat. + + Returns + ------- + func_out : function + If the image dimentionality is greater than 2D, the ndimage + function is returned, otherwise skimage function is used. + """ + def func_out(image, selem=None, out=None, **kwargs): + """Select a function appropriate for the image dimensionality + + Parameters + ---------- + image : ndarray + Image array. + selem : ndarray, optional + The neighborhood expressed as a 2-D array of 1's and 0's. + If None, use cross-shaped structuring element (connectivity=1). + out : ndarray of bool, optional + The array to store the result of the morphology. If None is + passed, a new array will be allocated. + + Returns + ------- + func_out : function + If the image dimentionality is greater than 2D, the ndimage + function is returned, otherwise skimage function is used. + """ + # Default structure element if selem is None: selem = _default_selem(image.ndim) From d9909c4db927091fe9b6e6d30c27ecb6bb23a162 Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Sun, 13 Jul 2014 12:36:05 -0700 Subject: [PATCH 0142/1122] Add tests for ndimage fallback for 3D white_tophat & black_tophat --- skimage/morphology/tests/test_grey.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index 191613c4..f9a7305d 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -166,6 +166,25 @@ def test_3d_fallback_cube_selem(): new_image = function(image, cube) yield testing.assert_array_equal, new_image, image +def test_3d_fallback_white_tophat(): + image = np.zeros((7, 7, 7), dtype=bool) + image[2, 2:4, 2:4] = 1 + image[3, 2:5, 2:5] = 1 + image[4, 3:5, 3:5] = 1 + new_image = grey.white_tophat(image) + footprint = ndimage.generate_binary_structure(3,1) + image_expected = ndimage.white_tophat(image,footprint=footprint) + testing.assert_array_equal(new_image, image_expected) + +def test_3d_fallback_black_tophat(): + image = np.ones((7, 7, 7), dtype=bool) + image[2, 2:4, 2:4] = 0 + image[3, 2:5, 2:5] = 0 + image[4, 3:5, 3:5] = 0 + new_image = grey.black_tophat(image) + footprint = ndimage.generate_binary_structure(3,1) + image_expected = ndimage.black_tophat(image,footprint=footprint) + testing.assert_array_equal(new_image, image_expected) class TestDTypes(): From 042d85ad435ab61092c1a05cf2733c6f4333aa41 Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Sun, 13 Jul 2014 16:31:58 -0700 Subject: [PATCH 0143/1122] Adjust hanging indentation whitespace in dict comprehension --- skimage/morphology/misc.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index db922fd7..26d40a72 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -5,11 +5,11 @@ from .selem import _default_selem # Our function names don't exactly correspond to ndimages. # These dictionaries translate from our names to scipy's. skimage2ndimage = {x: 'grey_' + x - for x in ('erosion','dilation','opening','closing')} + for x in ('erosion','dilation','opening','closing')} skimage2ndimage.update({x: x - for x in ('binary_erosion','binary_dilation', - 'binary_opening','binary_closing', - 'black_tophat','white_tophat')}) + for x in ('binary_erosion','binary_dilation', + 'binary_opening','binary_closing', + 'black_tophat','white_tophat')}) def default_fallback(func): """Decorator to fall back on ndimage for images with more than 2 dimensions From b677c400df7c37293f62f285776b928e59af3bcf Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Sun, 13 Jul 2014 16:55:06 -0700 Subject: [PATCH 0144/1122] Add whitespace following comma --- skimage/morphology/misc.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index 26d40a72..40ec6c25 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -3,13 +3,14 @@ import scipy.ndimage as nd from .selem import _default_selem # Our function names don't exactly correspond to ndimages. -# These dictionaries translate from our names to scipy's. -skimage2ndimage = {x: 'grey_' + x - for x in ('erosion','dilation','opening','closing')} -skimage2ndimage.update({x: x - for x in ('binary_erosion','binary_dilation', - 'binary_opening','binary_closing', - 'black_tophat','white_tophat')}) +# This dictionary translates from our names to scipy's. +funcs = ('erosion', 'dilation', 'opening', 'closing') +skimage2ndimage = {x: 'grey_' + x for x in funcs} + +# These function names are the same in ndimage. +funcs = ('binary_erosion', 'binary_dilation', 'binary_opening', + 'binary_closing', 'black_tophat', 'white_tophat') +skimage2ndimage.update({x: x for x in funcs}) def default_fallback(func): """Decorator to fall back on ndimage for images with more than 2 dimensions From a1519a015ead5d164a2d2c87a61c7ee41fbf5502 Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Sun, 13 Jul 2014 19:59:43 -0700 Subject: [PATCH 0145/1122] Change binary morphology functions to output numpy.bool dtype --- skimage/morphology/binary.py | 4 +-- skimage/morphology/tests/test_binary.py | 40 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index 5324b0f5..69d3eb08 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -49,7 +49,7 @@ def binary_erosion(image, selem=None, out=None): ndimage.convolve(binary, selem, mode='constant', cval=1, output=conv) if out is None: - out = conv + out = np.empty_like(conv, dtype=np.bool) return np.equal(conv, selem_sum, out=out) @@ -98,7 +98,7 @@ def binary_dilation(image, selem=None, out=None): ndimage.convolve(binary, selem, mode='constant', cval=0, output=conv) if out is None: - out = conv + out = np.empty_like(conv, dtype=np.bool) return np.not_equal(conv, 0, out=out) diff --git a/skimage/morphology/tests/test_binary.py b/skimage/morphology/tests/test_binary.py index c0c4c682..e0db7416 100644 --- a/skimage/morphology/tests/test_binary.py +++ b/skimage/morphology/tests/test_binary.py @@ -110,5 +110,45 @@ def test_3d_fallback_cube_selem(): new_image = function(image, cube) yield testing.assert_array_equal, new_image, image +def test_binary_output_2d(): + image = np.zeros((9, 9), np.uint16) + image[2:-2, 2:-2] = 2**14 + image[3:-3, 3:-3] = 2**15 + image[4, 4] = 2**16-1 + + bin_opened = binary.binary_opening(image) + bin_closed = binary.binary_closing(image) + + int_opened = np.empty_like(image, dtype=np.uint8) + int_closed = np.empty_like(image, dtype=np.uint8) + binary.binary_opening(image, out=int_opened) + binary.binary_closing(image, out=int_closed) + + testing.assert_equal(bin_opened.dtype, np.bool) + testing.assert_equal(bin_closed.dtype, np.bool) + + testing.assert_equal(int_opened.dtype, np.uint8) + testing.assert_equal(int_closed.dtype, np.uint8) + +def test_binary_output_3d(): + image = np.zeros((9, 9, 9), np.uint16) + image[2:-2, 2:-2, 2:-2] = 2**14 + image[3:-3, 3:-3, 3:-3] = 2**15 + image[4, 4, 4] = 2**16-1 + + bin_opened = binary.binary_opening(image) + bin_closed = binary.binary_closing(image) + + int_opened = np.empty_like(image, dtype=np.uint8) + int_closed = np.empty_like(image, dtype=np.uint8) + binary.binary_opening(image, out=int_opened) + binary.binary_closing(image, out=int_closed) + + testing.assert_equal(bin_opened.dtype, np.bool) + testing.assert_equal(bin_closed.dtype, np.bool) + + testing.assert_equal(int_opened.dtype, np.uint8) + testing.assert_equal(int_closed.dtype, np.uint8) + if __name__ == '__main__': testing.run_module_suite() From 21364e8741da905c9e382118cca780ff47d1776e Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 14 Jul 2014 23:06:54 +0530 Subject: [PATCH 0146/1122] Fixed threshold in example --- doc/examples/plot_rag_mean_color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/examples/plot_rag_mean_color.py b/doc/examples/plot_rag_mean_color.py index 7a1d24b9..ab962e65 100644 --- a/doc/examples/plot_rag_mean_color.py +++ b/doc/examples/plot_rag_mean_color.py @@ -19,7 +19,7 @@ labels1 = segmentation.slic(img, compactness=30, n_segments=400) out1 = color.label2rgb(labels1, img, kind='avg') g = graph.rag_mean_color(img, labels1) -labels2 = graph.cut_threshold(labels1, g, 30) +labels2 = graph.cut_threshold(labels1, g, 29) out2 = color.label2rgb(labels2, img, kind='avg') plt.figure() From f262a71dc5adc348d9149d7b02c833a159d68845 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 15 Jul 2014 03:46:26 -0500 Subject: [PATCH 0147/1122] Document NetworkX dependency in DEPENDS.txt --- DEPENDS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/DEPENDS.txt b/DEPENDS.txt index 152e935b..eb7cc09c 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -14,6 +14,7 @@ You can use pip to automatically install the base dependencies as follows:: Runtime requirements -------------------- * `SciPy >= 0.10 `__ +* `NetworkX >= 1.8 `__ Known build errors ------------------ From 001a63bd21c3afe652458b021bcefeddb760831d Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Tue, 15 Jul 2014 08:03:31 -0700 Subject: [PATCH 0148/1122] Remove repeated comments --- skimage/morphology/binary.py | 12 ------------ skimage/morphology/grey.py | 24 ------------------------ 2 files changed, 36 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index 69d3eb08..216e3690 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -53,10 +53,6 @@ def binary_erosion(image, selem=None, out=None): return np.equal(conv, selem_sum, out=out) -# Our functions only work in 2D, so for 3D or higher input we should fall back -# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring -# element of the appropriate dimension for each of these functions. -# The `default_callback` provides all these. @default_fallback def binary_dilation(image, selem=None, out=None): """Return fast binary morphological dilation of an image. @@ -102,10 +98,6 @@ def binary_dilation(image, selem=None, out=None): return np.not_equal(conv, 0, out=out) -# Our functions only work in 2D, so for 3D or higher input we should fall back -# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring -# element of the appropriate dimension for each of these functions. -# The `default_callback` provides all these. @default_fallback def binary_opening(image, selem=None, out=None): """Return fast binary morphological opening of an image. @@ -140,10 +132,6 @@ def binary_opening(image, selem=None, out=None): return out -# Our functions only work in 2D, so for 3D or higher input we should fall back -# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring -# element of the appropriate dimension for each of these functions. -# The `default_callback` provides all these. @default_fallback def binary_closing(image, selem=None, out=None): """Return fast binary morphological closing of an image. diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index e09872cb..19a4a346 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -8,10 +8,6 @@ from . import cmorph __all__ = ['erosion', 'dilation', 'opening', 'closing', 'white_tophat', 'black_tophat'] -# Our functions only work in 2D, so for 3D or higher input we should fall back -# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring -# element of the appropriate dimension for each of these functions. -# The `default_callback` provides all these. @default_fallback def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): """Return greyscale morphological erosion of an image. @@ -66,10 +62,6 @@ def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): shift_x=shift_x, shift_y=shift_y) -# Our functions only work in 2D, so for 3D or higher input we should fall back -# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring -# element of the appropriate dimension for each of these functions. -# The `default_callback` provides all these. @default_fallback def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): """Return greyscale morphological dilation of an image. @@ -126,10 +118,6 @@ def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): shift_x=shift_x, shift_y=shift_y) -# Our functions only work in 2D, so for 3D or higher input we should fall back -# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring -# element of the appropriate dimension for each of these functions. -# The `default_callback` provides all these. @default_fallback def opening(image, selem=None, out=None): """Return greyscale morphological opening of an image. @@ -183,10 +171,6 @@ def opening(image, selem=None, out=None): return out -# Our functions only work in 2D, so for 3D or higher input we should fall back -# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring -# element of the appropriate dimension for each of these functions. -# The `default_callback` provides all these. @default_fallback def closing(image, selem=None, out=None): """Return greyscale morphological closing of an image. @@ -240,10 +224,6 @@ def closing(image, selem=None, out=None): return out -# Our functions only work in 2D, so for 3D or higher input we should fall back -# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring -# element of the appropriate dimension for each of these functions. -# The `default_callback` provides all these. @default_fallback def white_tophat(image, selem=None, out=None): """Return white top hat of an image. @@ -295,10 +275,6 @@ def white_tophat(image, selem=None, out=None): return out -# Our functions only work in 2D, so for 3D or higher input we should fall back -# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring -# element of the appropriate dimension for each of these functions. -# The `default_callback` provides all these. @default_fallback def black_tophat(image, selem=None, out=None): """Return black top hat of an image. From 8c2b9b1784efbe43fe01da6ebea81643673a853b Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Tue, 15 Jul 2014 08:07:30 -0700 Subject: [PATCH 0149/1122] Updated docstrings --- skimage/morphology/misc.py | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index 40ec6c25..181fe5a4 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -14,7 +14,9 @@ skimage2ndimage.update({x: x for x in funcs}) def default_fallback(func): """Decorator to fall back on ndimage for images with more than 2 dimensions - + Decorator also provides a default structuring element, `selem`, with the + appropriate dimensionality if none is specified. + Parameters ---------- func : function @@ -29,26 +31,6 @@ def default_fallback(func): """ def func_out(image, selem=None, out=None, **kwargs): - """Select a function appropriate for the image dimensionality - - Parameters - ---------- - image : ndarray - Image array. - selem : ndarray, optional - The neighborhood expressed as a 2-D array of 1's and 0's. - If None, use cross-shaped structuring element (connectivity=1). - out : ndarray of bool, optional - The array to store the result of the morphology. If None is - passed, a new array will be allocated. - - Returns - ------- - func_out : function - If the image dimentionality is greater than 2D, the ndimage - function is returned, otherwise skimage function is used. - """ - # Default structure element if selem is None: selem = _default_selem(image.ndim) From 7976520e7af7102e76fd9139617b7c953535a623 Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Tue, 15 Jul 2014 08:25:17 -0700 Subject: [PATCH 0150/1122] Add test that the 2D results correspond to scipy's Remove trailing whitespace --- skimage/morphology/misc.py | 7 ++--- skimage/morphology/tests/test_binary.py | 34 ++++++++++++++++++------- skimage/morphology/tests/test_grey.py | 18 ++++++++++++- 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index 181fe5a4..94aadf7a 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -8,15 +8,16 @@ funcs = ('erosion', 'dilation', 'opening', 'closing') skimage2ndimage = {x: 'grey_' + x for x in funcs} # These function names are the same in ndimage. -funcs = ('binary_erosion', 'binary_dilation', 'binary_opening', +funcs = ('binary_erosion', 'binary_dilation', 'binary_opening', 'binary_closing', 'black_tophat', 'white_tophat') skimage2ndimage.update({x: x for x in funcs}) def default_fallback(func): """Decorator to fall back on ndimage for images with more than 2 dimensions + Decorator also provides a default structuring element, `selem`, with the appropriate dimensionality if none is specified. - + Parameters ---------- func : function @@ -29,7 +30,7 @@ def default_fallback(func): If the image dimentionality is greater than 2D, the ndimage function is returned, otherwise skimage function is used. """ - + def func_out(image, selem=None, out=None, **kwargs): # Default structure element if selem is None: diff --git a/skimage/morphology/tests/test_binary.py b/skimage/morphology/tests/test_binary.py index e0db7416..a8284103 100644 --- a/skimage/morphology/tests/test_binary.py +++ b/skimage/morphology/tests/test_binary.py @@ -110,26 +110,42 @@ def test_3d_fallback_cube_selem(): new_image = function(image, cube) yield testing.assert_array_equal, new_image, image +def test_2d_ndimage_equivalence(): + image = np.zeros((9, 9), np.uint16) + image[2:-2, 2:-2] = 2**14 + image[3:-3, 3:-3] = 2**15 + image[4, 4] = 2**16-1 + + bin_opened = binary.binary_opening(image) + bin_closed = binary.binary_closing(image) + + selem = ndimage.generate_binary_structure(2, 1) + ndimage_opened = ndimage.binary_opening(image, structure=selem) + ndimage_closed = ndimage.binary_closing(image, structure=selem) + + testing.assert_array_equal(bin_opened, ndimage_opened) + testing.assert_array_equal(bin_closed, ndimage_closed) + def test_binary_output_2d(): image = np.zeros((9, 9), np.uint16) image[2:-2, 2:-2] = 2**14 image[3:-3, 3:-3] = 2**15 image[4, 4] = 2**16-1 - + bin_opened = binary.binary_opening(image) bin_closed = binary.binary_closing(image) - + int_opened = np.empty_like(image, dtype=np.uint8) int_closed = np.empty_like(image, dtype=np.uint8) binary.binary_opening(image, out=int_opened) binary.binary_closing(image, out=int_closed) - + testing.assert_equal(bin_opened.dtype, np.bool) testing.assert_equal(bin_closed.dtype, np.bool) - + testing.assert_equal(int_opened.dtype, np.uint8) testing.assert_equal(int_closed.dtype, np.uint8) - + def test_binary_output_3d(): image = np.zeros((9, 9, 9), np.uint16) image[2:-2, 2:-2, 2:-2] = 2**14 @@ -138,17 +154,17 @@ def test_binary_output_3d(): bin_opened = binary.binary_opening(image) bin_closed = binary.binary_closing(image) - + int_opened = np.empty_like(image, dtype=np.uint8) int_closed = np.empty_like(image, dtype=np.uint8) binary.binary_opening(image, out=int_opened) binary.binary_closing(image, out=int_closed) - + testing.assert_equal(bin_opened.dtype, np.bool) testing.assert_equal(bin_closed.dtype, np.bool) - + testing.assert_equal(int_opened.dtype, np.uint8) - testing.assert_equal(int_closed.dtype, np.uint8) + testing.assert_equal(int_closed.dtype, np.uint8) if __name__ == '__main__': testing.run_module_suite() diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index f9a7305d..9f8e95b3 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -175,7 +175,7 @@ def test_3d_fallback_white_tophat(): footprint = ndimage.generate_binary_structure(3,1) image_expected = ndimage.white_tophat(image,footprint=footprint) testing.assert_array_equal(new_image, image_expected) - + def test_3d_fallback_black_tophat(): image = np.ones((7, 7, 7), dtype=bool) image[2, 2:4, 2:4] = 0 @@ -186,6 +186,22 @@ def test_3d_fallback_black_tophat(): image_expected = ndimage.black_tophat(image,footprint=footprint) testing.assert_array_equal(new_image, image_expected) +def test_2d_ndimage_equivalence(): + image = np.zeros((9, 9), np.uint16) + image[2:-2, 2:-2] = 2**14 + image[3:-3, 3:-3] = 2**15 + image[4, 4] = 2**16-1 + + opened = grey.opening(image) + closed = grey.closing(image) + + selem = ndimage.generate_binary_structure(2, 1) + ndimage_opened = ndimage.grey_opening(image, structure=selem) + ndimage_closed = ndimage.grey_closing(image, structure=selem) + + testing.assert_array_equal(opened, ndimage_opened) + testing.assert_array_equal(closed, ndimage_closed) + class TestDTypes(): def setUp(self): From 4449c7788402e46edbf839ce1718c45291c2ecb4 Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Tue, 15 Jul 2014 08:39:34 -0700 Subject: [PATCH 0151/1122] Fix calls to ndimage to use footprint argument --- skimage/morphology/tests/test_grey.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index 9f8e95b3..ecc0e89e 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -196,8 +196,8 @@ def test_2d_ndimage_equivalence(): closed = grey.closing(image) selem = ndimage.generate_binary_structure(2, 1) - ndimage_opened = ndimage.grey_opening(image, structure=selem) - ndimage_closed = ndimage.grey_closing(image, structure=selem) + ndimage_opened = ndimage.grey_opening(image, footprint=selem) + ndimage_closed = ndimage.grey_closing(image, footprint=selem) testing.assert_array_equal(opened, ndimage_opened) testing.assert_array_equal(closed, ndimage_closed) From 94bbb5827411b31323a795d08665b0d6011bc229 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Wed, 16 Jul 2014 14:06:08 -0500 Subject: [PATCH 0152/1122] Minor cleanup of adapthist module --- skimage/exposure/_adapthist.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/skimage/exposure/_adapthist.py b/skimage/exposure/_adapthist.py index 22b36542..39d35b54 100644 --- a/skimage/exposure/_adapthist.py +++ b/skimage/exposure/_adapthist.py @@ -22,7 +22,7 @@ from skimage.util import view_as_blocks MAX_REG_X = 16 # max. # contextual regions in x-direction */ MAX_REG_Y = 16 # max. # contextual regions in y-direction */ -NR_OF_GREY = 16384 # number of grayscale levels to use in CLAHE algorithm +NR_OF_GREY = 2**14 # number of grayscale levels to use in CLAHE algorithm def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, @@ -34,9 +34,9 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, image : array-like Input image. ntiles_x : int, optional - Number of tile regions in the X direction. Ranges between 2 and 16. + Number of tile regions in the X direction. Ranges between 1 and 16. ntiles_y : int, optional - Number of tile regions in the Y direction. Ranges between 2 and 16. + Number of tile regions in the Y direction. Ranges between 1 and 16. clip_limit : float: optional Clipping limit, normalized between 0 and 1 (higher values give more contrast). @@ -115,8 +115,6 @@ def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): """ ntiles_x = min(ntiles_x, MAX_REG_X) ntiles_y = min(ntiles_y, MAX_REG_Y) - ntiles_y = max(ntiles_y, 2) - ntiles_x = max(ntiles_x, 2) if clip_limit == 1.0: return image # is OK, immediately returns original image. @@ -125,10 +123,8 @@ def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): w_inner = image.shape[1] - image.shape[1] % ntiles_x # make the tile size divisible by 2 - while h_inner % (2 * ntiles_y): - h_inner -= 1 - while w_inner % (2 * ntiles_x): - w_inner -= 1 + h_inner -= h_inner % (2 * ntiles_y) + w_inner -= w_inner % (2 * ntiles_x) orig_shape = image.shape width = w_inner // ntiles_x # Actual size of contextual regions From 736de2c4032843279e61a5433ad37ab46a46c8d1 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Wed, 16 Jul 2014 14:10:10 -0500 Subject: [PATCH 0153/1122] Rename module to be more explicit --- skimage/viewer/tests/{test_viewer.py => test_tools.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename skimage/viewer/tests/{test_viewer.py => test_tools.py} (100%) diff --git a/skimage/viewer/tests/test_viewer.py b/skimage/viewer/tests/test_tools.py similarity index 100% rename from skimage/viewer/tests/test_viewer.py rename to skimage/viewer/tests/test_tools.py From 9576df45f8695f9d4303d74654d80f8a14daf5b3 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Wed, 16 Jul 2014 19:38:23 -0500 Subject: [PATCH 0154/1122] Add tests for all of viewer subpackage, with necessary file mods --- skimage/viewer/canvastools/base.py | 2 +- skimage/viewer/canvastools/linetool.py | 2 +- skimage/viewer/canvastools/painttool.py | 2 +- skimage/viewer/canvastools/recttool.py | 2 +- skimage/viewer/plugins/base.py | 3 +- skimage/viewer/tests/test_plugins.py | 179 ++++++++++++++++++ skimage/viewer/tests/test_tools.py | 230 ++++++++++++++++++++---- skimage/viewer/tests/test_utils.py | 36 ++++ skimage/viewer/tests/test_viewer.py | 77 ++++++++ skimage/viewer/tests/test_widgets.py | 105 +++++++++++ skimage/viewer/viewers/core.py | 10 +- skimage/viewer/widgets/history.py | 5 +- 12 files changed, 607 insertions(+), 46 deletions(-) create mode 100644 skimage/viewer/tests/test_plugins.py create mode 100644 skimage/viewer/tests/test_utils.py create mode 100644 skimage/viewer/tests/test_viewer.py create mode 100644 skimage/viewer/tests/test_widgets.py diff --git a/skimage/viewer/canvastools/base.py b/skimage/viewer/canvastools/base.py index 6fcda9c4..7f61ce2c 100644 --- a/skimage/viewer/canvastools/base.py +++ b/skimage/viewer/canvastools/base.py @@ -115,7 +115,7 @@ class CanvasToolBase(object): @property def geometry(self): """Geometry information that gets passed to callback functions.""" - raise NotImplementedError + return None class ToolHandles(object): diff --git a/skimage/viewer/canvastools/linetool.py b/skimage/viewer/canvastools/linetool.py index c32d0ea7..d777e50b 100644 --- a/skimage/viewer/canvastools/linetool.py +++ b/skimage/viewer/canvastools/linetool.py @@ -191,7 +191,7 @@ class ThickLineTool(LineTool): self.callback_on_change(self.geometry) -if __name__ == '__main__': +if __name__ == '__main__': # pragma: no cover import matplotlib.pyplot as plt from skimage import data diff --git a/skimage/viewer/canvastools/painttool.py b/skimage/viewer/canvastools/painttool.py index 66cec08d..6af8b5d3 100644 --- a/skimage/viewer/canvastools/painttool.py +++ b/skimage/viewer/canvastools/painttool.py @@ -198,7 +198,7 @@ class CenteredWindow(object): return [slice(ymin, ymax), slice(xmin, xmax)] -if __name__ == '__main__': +if __name__ == '__main__': # pragma: no cover np.testing.rundocs() from skimage import data diff --git a/skimage/viewer/canvastools/recttool.py b/skimage/viewer/canvastools/recttool.py index d4ae113e..f055e439 100644 --- a/skimage/viewer/canvastools/recttool.py +++ b/skimage/viewer/canvastools/recttool.py @@ -201,7 +201,7 @@ class RectangleTool(CanvasToolBase, RectangleSelector): return self.extents -if __name__ == '__main__': +if __name__ == '__main__': # pragma: no cover import matplotlib.pyplot as plt from skimage import data diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index baded1cb..6b162fd8 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -239,7 +239,8 @@ class Plugin(QtGui.QDialog): def clean_up(self): self.remove_image_artists() - self.image_viewer.plugins.remove(self) + if self in self.image_viewer.plugins: + self.image_viewer.plugins.remove(self) self.image_viewer.reset_image() self.image_viewer.redraw() diff --git a/skimage/viewer/tests/test_plugins.py b/skimage/viewer/tests/test_plugins.py new file mode 100644 index 00000000..3f5eb575 --- /dev/null +++ b/skimage/viewer/tests/test_plugins.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +import numpy as np +import skimage +import skimage.data as data +from skimage.filter.rank import median +from skimage.morphology import disk +from skimage.viewer import ImageViewer +from skimage.viewer.qt import qt_api +from numpy.testing import assert_equal, assert_allclose, assert_almost_equal +from numpy.testing.decorators import skipif +from skimage.viewer.plugins import ( + LineProfile, Measure, CannyPlugin, LabelPainter, Crop, ColorHistogram, + PlotPlugin) +from skimage.viewer.plugins.base import Plugin +from skimage.viewer.widgets import Slider + + +def setup_line_profile(image, limits='image'): + viewer = ImageViewer(skimage.img_as_float(image)) + plugin = LineProfile(limits) + viewer += plugin + return plugin + + +@skipif(qt_api is None) +def test_line_profile(): + """ Test a line profile using an ndim=2 image""" + plugin = setup_line_profile(data.camera()) + line_image, scan_data = plugin.output() + for inp in [line_image.nonzero()[0].size, + line_image.sum() / line_image.max(), + scan_data.size]: + assert_equal(inp, 172) + assert_equal(line_image.shape, (512, 512)) + assert_allclose(scan_data.max(), 0.9139, rtol=1e-3) + assert_allclose(scan_data.mean(), 0.2828, rtol=1e-3) + + +@skipif(qt_api is None) +def test_line_profile_rgb(): + """ Test a line profile using an ndim=3 image""" + plugin = setup_line_profile(data.chelsea(), limits=None) + for i in range(6): + plugin.line_tool._thicken_scan_line() + line_image, scan_data = plugin.output() + assert_equal(line_image[line_image == 128].size, 755) + assert_equal(line_image[line_image == 255].size, 151) + assert_equal(line_image.shape, (300, 451)) + assert_equal(scan_data.shape, (152, 3)) + assert_allclose(scan_data.max(), 0.772, rtol=1e-3) + assert_allclose(scan_data.mean(), 0.4355, rtol=1e-3) + + +@skipif(qt_api is None) +def test_line_profile_dynamic(): + """Test a line profile updating after an image transform""" + image = data.coins()[:-50, :] # shave some off to make the line lower + image = skimage.img_as_float(image) + viewer = ImageViewer(image) + + lp = LineProfile(limits='dtype') + viewer += lp + + line = lp.get_profiles()[-1][0] + assert line.size == 129 + assert_almost_equal(np.std(image), 46.478, 3) + assert_almost_equal(np.std(line), 0.226, 3) + assert_almost_equal(np.max(line) - np.min(line), 0.725, 1) + + viewer.image = median(image) + + line = lp.get_profiles()[-1][0] + assert_almost_equal(np.std(image), 51.364, 3) + assert_almost_equal(np.std(line), 56.3555, 3) + assert_almost_equal(np.max(line) - np.min(line), 172.0, 1) + + +@skipif(qt_api is None) +def test_measure(): + image = data.camera() + viewer = ImageViewer(image) + m = Measure() + viewer += m + + m.line_changed([(0, 0), (10, 10)]) + assert_equal(str(m._length.text), '14.1') + assert_equal(str(m._angle.text[:5]), u'135.0') + + +@skipif(qt_api is None) +def test_canny(): + image = data.camera() + viewer = ImageViewer(image) + c = CannyPlugin() + viewer += c + + canny_edges = viewer.show(False) + viewer.close() + edges = canny_edges[0][0] + assert edges.sum() == 2852 + + +@skipif(qt_api is None) +def test_label_painter(): + image = data.camera() + moon = data.moon() + viewer = ImageViewer(image) + lp = LabelPainter() + viewer += lp + + assert_equal(lp.radius, 5) + lp.label = 1 + assert_equal(lp.label, '1') + lp.label = 2 + assert_equal(lp.label, '2') + assert_equal(lp.paint_tool.radius, 2) + lp._on_new_image(moon) + assert_equal(lp.paint_tool.shape, moon.shape) + + +@skipif(qt_api is None) +def test_crop(): + image = data.camera() + viewer = ImageViewer(image) + c = Crop() + viewer += c + + c.crop((0, 100, 0, 100)) + assert_equal(viewer.image.shape, (101, 101)) + + +@skipif(qt_api is None) +def test_color_histogram(): + image = skimage.img_as_float(data.load('color.png')) + viewer = ImageViewer(image) + ch = ColorHistogram(dock='right') + viewer += ch + + assert_almost_equal(viewer.image.std(), 0.352, 3), + ch.ab_selected((0, 100, 0, 100)), + assert_almost_equal(viewer.image.std(), 0.325, 3) + + +@skipif(qt_api is None) +def test_plot_plugin(): + viewer = ImageViewer(data.moon()) + plugin = PlotPlugin(image_filter=lambda x: x) + viewer += plugin + + assert_equal(viewer.image, data.moon()) + plugin._update_original_image(data.coins()) + assert_equal(viewer.image, data.coins()) + viewer.close() + + +@skipif(qt_api is None) +def test_plugin(): + img = skimage.img_as_float(data.moon()) + viewer = ImageViewer(img) + + def median_filter(img, radius=3): + return median(img, selem=disk(radius=radius)) + + plugin = Plugin(image_filter=median_filter) + viewer += plugin + + plugin += Slider('radius', 1, 5) + + assert_almost_equal(np.std(viewer.image), 12.556, 3) + + plugin.filter_image() + + assert_almost_equal(np.std(viewer.image), 12.931, 3) + + plugin.show() + plugin.close() + plugin.clean_up() + img, _ = plugin.output() + assert_equal(img, viewer.image) diff --git a/skimage/viewer/tests/test_tools.py b/skimage/viewer/tests/test_tools.py index 7fa4e374..853dd3d8 100644 --- a/skimage/viewer/tests/test_tools.py +++ b/skimage/viewer/tests/test_tools.py @@ -1,48 +1,208 @@ -import skimage -import skimage.data as data +from collections import namedtuple + +import numpy as np +from skimage import data from skimage.viewer import ImageViewer +from skimage.viewer.canvastools import ( + LineTool, ThickLineTool, RectangleTool, PaintTool) +from skimage.viewer.canvastools.base import CanvasToolBase from skimage.viewer.qt import qt_api -from numpy.testing import assert_equal, assert_allclose +from numpy.testing import assert_equal from numpy.testing.decorators import skipif -def setup_line_profile(image): - from skimage.viewer.plugins.lineprofile import LineProfile - viewer = ImageViewer(skimage.img_as_float(image)) - plugin = LineProfile() - viewer += plugin - return plugin +def get_end_points(image): + h, w = image.shape[0:2] + x = [w / 3, 2 * w / 3] + y = [h / 2] * 2 + return np.transpose([x, y]) + + +def create_mouse_event(ax, button=1, xdata=0, ydata=0, key=None): + """ + *name* + the event name + + *canvas* + the FigureCanvas instance generating the event + + *guiEvent* + the GUI event that triggered the matplotlib event + + *x* + x position - pixels from left of canvas + + *y* + y position - pixels from bottom of canvas + + *inaxes* + the :class:`~matplotlib.axes.Axes` instance if mouse is over axes + + *xdata* + x coord of mouse in data coords + + *ydata* + y coord of mouse in data coords + + *button* + button pressed None, 1, 2, 3, 'up', 'down' (up and down are used + for scroll events) + + *key* + the key depressed when the mouse event triggered (see + :class:`KeyEvent`) + + *step* + number of scroll steps (positive for 'up', negative for 'down') + """ + event = namedtuple('Event', + ('name canvas guiEvent x y inaxes xdata ydata ' + 'button key step')) + event.button = button + event.x, event.y = ax.transData.transform((xdata, ydata)) + event.xdata, event.ydata = xdata, ydata + event.inaxes = ax + event.canvas = ax.figure.canvas + event.key = key + event.step = 1 + event.guiEvent = None + event.name = 'Custom' + return event @skipif(qt_api is None) -def test_line_profile(): - """ Test a line profile using an ndim=2 image""" - plugin = setup_line_profile(data.camera()) - line_image, scan_data = plugin.output() - for inp in [line_image.nonzero()[0].size, - line_image.sum() / line_image.max(), - scan_data.size]: - assert_equal(inp, 172) - assert_equal(line_image.shape, (512, 512)) - assert_allclose(scan_data.max(), 0.9139, rtol=1e-3) - assert_allclose(scan_data.mean(), 0.2828, rtol=1e-3) +def test_line_tool(): + img = data.camera() + viewer = ImageViewer(img) + + tool = LineTool(viewer.ax, maxdist=10) + tool.end_points = get_end_points(img) + assert_equal(tool.end_points, np.array([[170, 256], [341, 256]])) + + # grab a handle and move it + grab = create_mouse_event(viewer.ax, xdata=170, ydata=256) + tool.on_mouse_press(grab) + move = create_mouse_event(viewer.ax, xdata=180, ydata=260) + tool.on_move(move) + tool.on_mouse_release(move) + assert_equal(tool.geometry, np.array([[180, 260], [341, 256]])) + + # create a new line + new = create_mouse_event(viewer.ax, xdata=10, ydata=10) + tool.on_mouse_press(new) + move = create_mouse_event(viewer.ax, xdata=100, ydata=100) + tool.on_move(move) + tool.on_mouse_release(move) + assert_equal(tool.geometry, np.array([[100, 100], [10, 10]])) @skipif(qt_api is None) -def test_line_profile_rgb(): - """ Test a line profile using an ndim=3 image""" - plugin = setup_line_profile(data.chelsea()) - for i in range(6): - plugin.line_tool._thicken_scan_line() - line_image, scan_data = plugin.output() - assert_equal(line_image[line_image == 128].size, 755) - assert_equal(line_image[line_image == 255].size, 151) - assert_equal(line_image.shape, (300, 451)) - assert_equal(scan_data.shape, (152, 3)) - assert_allclose(scan_data.max(), 0.772, rtol=1e-3) - assert_allclose(scan_data.mean(), 0.4355, rtol=1e-3) +def test_thick_line_tool(): + img = data.camera() + viewer = ImageViewer(img) + + tool = ThickLineTool(viewer.ax, maxdist=10) + tool.end_points = get_end_points(img) + + scroll_up = create_mouse_event(viewer.ax, button='up') + tool.on_scroll(scroll_up) + assert_equal(tool.linewidth, 2) + + scroll_down = create_mouse_event(viewer.ax, button='down') + tool.on_scroll(scroll_down) + assert_equal(tool.linewidth, 1) + + key_up = create_mouse_event(viewer.ax, key='+') + tool.on_key_press(key_up) + assert_equal(tool.linewidth, 2) + + key_down = create_mouse_event(viewer.ax, key='-') + tool.on_key_press(key_down) + assert_equal(tool.linewidth, 1) -if __name__ == "__main__": - from numpy.testing import run_module_suite - run_module_suite() +@skipif(qt_api is None) +def test_rect_tool(): + img = data.camera() + viewer = ImageViewer(img) + + tool = RectangleTool(viewer.ax, maxdist=10) + tool.extents = (100, 150, 100, 150) + + assert_equal(tool.corners, + ((100, 150, 150, 100), (100, 100, 150, 150))) + assert_equal(tool.extents, (100, 150, 100, 150)) + assert_equal(tool.edge_centers, + ((100, 125.0, 150, 125.0), (125.0, 100, 125.0, 150))) + assert_equal(tool.geometry, (100, 150, 100, 150)) + + # grab a corner and move it + grab = create_mouse_event(viewer.ax, xdata=100, ydata=100) + tool.press(grab) + move = create_mouse_event(viewer.ax, xdata=120, ydata=120) + tool.onmove(move) + tool.release(move) + assert_equal(tool.geometry, [120, 150, 120, 150]) + + # create a new line + new = create_mouse_event(viewer.ax, xdata=10, ydata=10) + tool.press(new) + move = create_mouse_event(viewer.ax, xdata=100, ydata=100) + tool.onmove(move) + tool.release(move) + assert_equal(tool.geometry, [10, 100, 10, 100]) + + +@skipif(qt_api is None) +def test_paint_tool(): + img = data.moon() + viewer = ImageViewer(img) + + tool = PaintTool(viewer.ax, img.shape) + + tool.radius = 10 + assert_equal(tool.radius, 10) + tool.label = 2 + assert_equal(tool.label, 2) + assert_equal(tool.shape, img.shape) + + start = create_mouse_event(viewer.ax, xdata=100, ydata=100) + tool.on_mouse_press(start) + move = create_mouse_event(viewer.ax, xdata=110, ydata=110) + tool.on_move(move) + tool.on_mouse_release(move) + assert_equal(tool.overlay[tool.overlay == 2].size, 761) + + tool.label = 5 + start = create_mouse_event(viewer.ax, xdata=20, ydata=20) + tool.on_mouse_press(start) + move = create_mouse_event(viewer.ax, xdata=40, ydata=40) + tool.on_move(move) + tool.on_mouse_release(move) + assert_equal(tool.overlay[tool.overlay == 5].size, 881) + assert_equal(tool.overlay[tool.overlay == 2].size, 761) + + enter = create_mouse_event(viewer.ax, key='enter') + tool.on_mouse_press(enter) + + tool.overlay = tool.overlay * 0 + assert_equal(tool.overlay.sum(), 0) + + +@skipif(qt_api is None) +def test_base_tool(): + img = data.moon() + viewer = ImageViewer(img) + + tool = CanvasToolBase(viewer.ax) + tool.set_visible(False) + tool.set_visible(True) + + enter = create_mouse_event(viewer.ax, key='enter') + tool._on_key_press(enter) + + tool.redraw() + tool.remove() + + tool = CanvasToolBase(viewer.ax, useblit=False) + tool.redraw() diff --git a/skimage/viewer/tests/test_utils.py b/skimage/viewer/tests/test_utils.py new file mode 100644 index 00000000..b72d99ab --- /dev/null +++ b/skimage/viewer/tests/test_utils.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +from skimage.viewer.qt import qt_api, QtCore, QtGui +from numpy.testing.decorators import skipif +from skimage.viewer import utils +from skimage.viewer.utils import dialogs + + +@skipif(qt_api is None) +def test_event_loop(): + utils.init_qtapp() + timer = QtCore.QTimer() + timer.singleShot(10, QtGui.QApplication.quit) + utils.start_qtapp() + + +def test_format_filename(): + fname = dialogs._format_filename(('apple', 2)) + assert fname == 'apple' + fname = dialogs._format_filename('') + assert fname is None + + +def test_open_file_dialog(): + utils.init_qtapp() + timer = QtCore.QTimer() + timer.singleShot(10, QtGui.QApplication.quit) + filename = dialogs.open_file_dialog() + assert filename is None + + +def test_save_file_dialog(): + utils.init_qtapp() + timer = QtCore.QTimer() + timer.singleShot(10, QtGui.QApplication.quit) + filename = dialogs.save_file_dialog() + assert filename is None diff --git a/skimage/viewer/tests/test_viewer.py b/skimage/viewer/tests/test_viewer.py new file mode 100644 index 00000000..00ef848c --- /dev/null +++ b/skimage/viewer/tests/test_viewer.py @@ -0,0 +1,77 @@ + +from skimage import data +from skimage.viewer import ImageViewer, CollectionViewer +from skimage.transform import pyramid_gaussian +from skimage.viewer.plugins import OverlayPlugin +from skimage.filter import sobel +from skimage.viewer.qt import qt_api, QtGui, QtCore +from numpy.testing import assert_equal +from numpy.testing.decorators import skipif + + +@skipif(qt_api is None) +def test_viewer(): + lena = data.lena() + coins = data.coins() + + view = ImageViewer(lena) + import tempfile + _, filename = tempfile.mkstemp(suffix='.png') + + view.show(False) + view.close() + view.save_to_file(filename) + view.open_file(filename) + assert_equal(view.image, lena) + view.image = coins + assert_equal(view.image, coins), + view.save_to_file(filename), + view.open_file(filename), + view.reset_image(), + assert_equal(view.image, coins) + + +def make_key_event(key): + return QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key, + QtCore.Qt.NoModifier) + + +@skipif(qt_api is None) +def test_collection_viewer(): + + img = data.lena() + img_collection = tuple(pyramid_gaussian(img)) + + view = CollectionViewer(img_collection) + make_key_event(48) + + view.update_index('', 2), + assert_equal(view.image, img_collection[2]) + view.keyPressEvent(make_key_event(53)) + assert_equal(view.image, img_collection[5]) + view._format_coord(10, 10) + + +@skipif(qt_api is None) +def test_viewer_with_overlay(): + img = data.coins() + ov = OverlayPlugin(image_filter=sobel) + viewer = ImageViewer(img) + viewer += ov + + import tempfile + _, filename = tempfile.mkstemp(suffix='.png') + + ov.color = 2 + assert_equal(ov.color, 'yellow') + viewer.save_to_file(filename) + ov.display_filtered_image(img) + assert_equal(ov.overlay, img) + ov.overlay = None + assert_equal(ov.overlay, None) + ov.overlay = img + assert_equal(ov.overlay, img) + assert_equal(ov.filtered_image, img) + + + diff --git a/skimage/viewer/tests/test_widgets.py b/skimage/viewer/tests/test_widgets.py new file mode 100644 index 00000000..a17465ee --- /dev/null +++ b/skimage/viewer/tests/test_widgets.py @@ -0,0 +1,105 @@ + +import os +from skimage import data, img_as_float, io +from skimage.viewer import ImageViewer +from skimage.viewer.widgets import ( + Slider, OKCancelButtons, SaveButtons, ComboBox, Text) +from skimage.viewer.plugins.base import Plugin + +from skimage.viewer.qt import qt_api, QtGui, QtCore +from numpy.testing import assert_almost_equal, assert_equal +from numpy.testing.decorators import skipif + + +def get_image_viewer(): + image = data.coins() + viewer = ImageViewer(img_as_float(image)) + viewer += Plugin() + return viewer + + +@skipif(qt_api is None) +def test_combo_box(): + viewer = get_image_viewer() + cb = ComboBox('hello', ('a', 'b', 'c')) + viewer.plugins[0] += cb + + assert_equal(cb.val, 'a') + assert_equal(cb.index, 0) + cb.index = 2 + assert_equal(cb.val, 'c'), + assert_equal(cb.index, 2) + + +@skipif(qt_api is None) +def test_text_widget(): + viewer = get_image_viewer() + txt = Text('hello', 'hello, world!') + viewer.plugins[0] += txt + + assert_equal(str(txt.text), 'hello, world!') + txt.text = 'goodbye, world!' + assert_equal(str(txt.text), 'goodbye, world!') + + +@skipif(qt_api is None) +def test_slider_int(): + viewer = get_image_viewer() + sld = Slider('radius', 2, 10, value_type='int') + viewer.plugins[0] += sld + + assert_equal(sld.val, 4) + sld.val = 6 + assert_equal(sld.val, 6) + sld.editbox.setText('5') + sld._on_editbox_changed() + assert_equal(sld.val, 5) + + +@skipif(qt_api is None) +def test_slider_float(): + viewer = get_image_viewer() + sld = Slider('alpha', 2.1, 3.1, value=2.1, value_type='float', + orientation='vertical', update_on='move') + viewer.plugins[0] += sld + + assert_equal(sld.val, 2.1) + sld.val = 2.5 + assert_almost_equal(sld.val, 2.5, 2) + sld.editbox.setText('0.1') + sld._on_editbox_changed() + assert_almost_equal(sld.val, 2.5, 2) + + +@skipif(qt_api is None) +def test_save_buttons(): + viewer = get_image_viewer() + sv = SaveButtons() + viewer.plugins[0] += sv + + import tempfile + _, filename = tempfile.mkstemp(suffix='.png') + os.remove(filename) + + timer = QtCore.QTimer() + timer.singleShot(100, lambda: QtGui.QApplication.quit()) + + sv.save_to_stack() + sv.save_to_file(filename) + + img = img_as_float(data.imread(filename)) + assert_almost_equal(img, viewer.image) + + img = io.pop() + assert_almost_equal(img, viewer.image) + + +@skipif(qt_api is None) +def test_ok_buttons(): + viewer = get_image_viewer() + ok = OKCancelButtons() + viewer.plugins[0] += ok + + ok.update_original_image(), + ok.close_plugin() + diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 32c10cd4..476017ef 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -174,9 +174,10 @@ class ImageViewer(QtGui.QMainWindow): h = viewer_size.height() self.resize(w + dx, h + dy) - def open_file(self): + def open_file(self, filename=None): """Open image file and display in viewer.""" - filename = dialogs.open_file_dialog() + if filename is None: + filename = dialogs.open_file_dialog() if filename is None: return image = io.imread(filename) @@ -187,7 +188,7 @@ class ImageViewer(QtGui.QMainWindow): self.image = image.copy() # update displayed image self.original_image_changed.emit(image) - def save_to_file(self): + def save_to_file(self, filename=None): """Save current image to file. The current behavior is not ideal: It saves the image displayed on @@ -195,7 +196,8 @@ class ImageViewer(QtGui.QMainWindow): not preserved (resizing the viewer window will alter the size of the saved image). """ - filename = dialogs.save_file_dialog() + if filename is None: + filename = dialogs.save_file_dialog() if filename is None: return if len(self.ax.images) == 1: diff --git a/skimage/viewer/widgets/history.py b/skimage/viewer/widgets/history.py index ce20b559..d1df8689 100644 --- a/skimage/viewer/widgets/history.py +++ b/skimage/viewer/widgets/history.py @@ -81,8 +81,9 @@ class SaveButtons(BaseWidget): NOTE: The io stack only works in interactive sessions.''') notify(msg) - def save_to_file(self): - filename = dialogs.save_file_dialog() + def save_to_file(self, filename=None): + if filename is None: + filename = dialogs.save_file_dialog() if filename is None: return image = self.plugin.filtered_image From 1a01f1a83b8e386276a53179b6e330cc74ea9061 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Wed, 16 Jul 2014 19:58:13 -0500 Subject: [PATCH 0155/1122] Fix failing tests --- skimage/viewer/plugins/lineprofile.py | 22 ++++++++++++---------- skimage/viewer/tests/test_plugins.py | 19 ++++++++++--------- skimage/viewer/tests/test_widgets.py | 7 ++++--- skimage/viewer/widgets/core.py | 2 +- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index d0011c75..6e5b2c1f 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -46,7 +46,7 @@ class LineProfile(PlotPlugin): if self._limit_type == 'image': self.limits = (np.min(image), np.max(image)) elif self._limit_type == 'dtype': - self._limit_type = dtype_range[image.dtype.type] + self.limits = dtype_range[image.dtype.type] elif self._limit_type is None or len(self._limit_type) == 2: self.limits = self._limit_type else: @@ -91,6 +91,7 @@ class LineProfile(PlotPlugin): profile: list of 1d arrays Profile of intensity values. Length 1 (grayscale) or 3 (rgb). """ + self._update_data() profiles = [data.get_ydata() for data in self.profile] return self.line_tool.end_points, profiles @@ -103,8 +104,15 @@ class LineProfile(PlotPlugin): def line_changed(self, end_points): x, y = np.transpose(end_points) self.line_tool.end_points = end_points - scan = measure.profile_line(self.image_viewer.original_image, - *end_points[:, ::-1], + self._update_data() + self.ax.relim() + + self._autoscale_view() + self.redraw() + + def _update_data(self): + scan = measure.profile_line(self.image_viewer.image, + *self.line_tool.end_points[:, ::-1], linewidth=self.line_tool.linewidth) self.scan_data = scan if scan.ndim == 1: @@ -117,11 +125,6 @@ class LineProfile(PlotPlugin): self.profile[i].set_xdata(np.arange(scan.shape[0])) self.profile[i].set_ydata(scan[:, i]) - self.ax.relim() - - self._autoscale_view() - self.redraw() - def reset_axes(self, scan_data): # Clear lines out for line in self.ax.lines: @@ -147,7 +150,7 @@ class LineProfile(PlotPlugin): The line scan values across the image. """ end_points = self.line_tool.end_points - line_image = np.zeros(self.image_viewer.original_image.shape[:2], + line_image = np.zeros(self.image_viewer.image.shape[:2], np.uint8) width = self.line_tool.linewidth if width > 1: @@ -162,4 +165,3 @@ class LineProfile(PlotPlugin): rr, cc = draw.line(y1, x1, y2, x2) line_image[rr, cc] = 255 return line_image, self.scan_data - diff --git a/skimage/viewer/tests/test_plugins.py b/skimage/viewer/tests/test_plugins.py index 3f5eb575..e8c16416 100644 --- a/skimage/viewer/tests/test_plugins.py +++ b/skimage/viewer/tests/test_plugins.py @@ -17,7 +17,7 @@ from skimage.viewer.widgets import Slider def setup_line_profile(image, limits='image'): viewer = ImageViewer(skimage.img_as_float(image)) - plugin = LineProfile(limits) + plugin = LineProfile(limits=limits) viewer += plugin return plugin @@ -63,16 +63,17 @@ def test_line_profile_dynamic(): line = lp.get_profiles()[-1][0] assert line.size == 129 - assert_almost_equal(np.std(image), 46.478, 3) + assert_almost_equal(np.std(viewer.image), 0.208, 3) assert_almost_equal(np.std(line), 0.226, 3) assert_almost_equal(np.max(line) - np.min(line), 0.725, 1) - viewer.image = median(image) + viewer.image = skimage.img_as_float(median(image, + selem=disk(radius=3))) line = lp.get_profiles()[-1][0] - assert_almost_equal(np.std(image), 51.364, 3) - assert_almost_equal(np.std(line), 56.3555, 3) - assert_almost_equal(np.max(line) - np.min(line), 172.0, 1) + assert_almost_equal(np.std(viewer.image), 0.198, 3) + assert_almost_equal(np.std(line), 0.220, 3) + assert_almost_equal(np.max(line) - np.min(line), 0.639, 1) @skipif(qt_api is None) @@ -110,10 +111,10 @@ def test_label_painter(): assert_equal(lp.radius, 5) lp.label = 1 - assert_equal(lp.label, '1') + assert_equal(str(lp.label), '1') lp.label = 2 - assert_equal(lp.label, '2') - assert_equal(lp.paint_tool.radius, 2) + assert_equal(str(lp.paint_tool.label), '2') + assert_equal(lp.paint_tool.radius, 5) lp._on_new_image(moon) assert_equal(lp.paint_tool.shape, moon.shape) diff --git a/skimage/viewer/tests/test_widgets.py b/skimage/viewer/tests/test_widgets.py index a17465ee..61450146 100644 --- a/skimage/viewer/tests/test_widgets.py +++ b/skimage/viewer/tests/test_widgets.py @@ -4,6 +4,7 @@ from skimage import data, img_as_float, io from skimage.viewer import ImageViewer from skimage.viewer.widgets import ( Slider, OKCancelButtons, SaveButtons, ComboBox, Text) +from skimage.viewer.utils import init_qtapp from skimage.viewer.plugins.base import Plugin from skimage.viewer.qt import qt_api, QtGui, QtCore @@ -24,10 +25,10 @@ def test_combo_box(): cb = ComboBox('hello', ('a', 'b', 'c')) viewer.plugins[0] += cb - assert_equal(cb.val, 'a') + assert_equal(str(cb.val), 'a') assert_equal(cb.index, 0) cb.index = 2 - assert_equal(cb.val, 'c'), + assert_equal(str(cb.val), 'c'), assert_equal(cb.index, 2) @@ -102,4 +103,4 @@ def test_ok_buttons(): ok.update_original_image(), ok.close_plugin() - + diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 2bbf53d2..74fe9de4 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -244,7 +244,7 @@ class ComboBox(BaseWidget): @property def val(self): - return self._combo_box.value() + return self._combo_box.currentText() @property def index(self): From 0b73d691440cf87e8633afb2861842f210d5429a Mon Sep 17 00:00:00 2001 From: blink1073 Date: Wed, 16 Jul 2014 20:39:37 -0500 Subject: [PATCH 0156/1122] Address travis failures --- skimage/viewer/canvastools/linetool.py | 2 +- skimage/viewer/tests/test_plugins.py | 14 +++++++------- skimage/viewer/tests/test_viewer.py | 6 +++--- skimage/viewer/tests/test_widgets.py | 1 - 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/skimage/viewer/canvastools/linetool.py b/skimage/viewer/canvastools/linetool.py index d777e50b..fed18383 100644 --- a/skimage/viewer/canvastools/linetool.py +++ b/skimage/viewer/canvastools/linetool.py @@ -69,7 +69,7 @@ class LineTool(CanvasToolBase): @property def end_points(self): - return self._end_pts + return self._end_pts.astype(int) @end_points.setter def end_points(self, pts): diff --git a/skimage/viewer/tests/test_plugins.py b/skimage/viewer/tests/test_plugins.py index e8c16416..59b1f3e4 100644 --- a/skimage/viewer/tests/test_plugins.py +++ b/skimage/viewer/tests/test_plugins.py @@ -32,8 +32,8 @@ def test_line_profile(): scan_data.size]: assert_equal(inp, 172) assert_equal(line_image.shape, (512, 512)) - assert_allclose(scan_data.max(), 0.9139, rtol=1e-3) - assert_allclose(scan_data.mean(), 0.2828, rtol=1e-3) + assert_allclose(scan_data.max(), 0.9176, rtol=1e-3) + assert_allclose(scan_data.mean(), 0.2812, rtol=1e-3) @skipif(qt_api is None) @@ -43,12 +43,12 @@ def test_line_profile_rgb(): for i in range(6): plugin.line_tool._thicken_scan_line() line_image, scan_data = plugin.output() - assert_equal(line_image[line_image == 128].size, 755) + assert_equal(line_image[line_image == 128].size, 750) assert_equal(line_image[line_image == 255].size, 151) assert_equal(line_image.shape, (300, 451)) - assert_equal(scan_data.shape, (152, 3)) + assert_equal(scan_data.shape, (151, 3)) assert_allclose(scan_data.max(), 0.772, rtol=1e-3) - assert_allclose(scan_data.mean(), 0.4355, rtol=1e-3) + assert_allclose(scan_data.mean(), 0.4359, rtol=1e-3) @skipif(qt_api is None) @@ -64,7 +64,7 @@ def test_line_profile_dynamic(): line = lp.get_profiles()[-1][0] assert line.size == 129 assert_almost_equal(np.std(viewer.image), 0.208, 3) - assert_almost_equal(np.std(line), 0.226, 3) + assert_almost_equal(np.std(line), 0.229, 3) assert_almost_equal(np.max(line) - np.min(line), 0.725, 1) viewer.image = skimage.img_as_float(median(image, @@ -85,7 +85,7 @@ def test_measure(): m.line_changed([(0, 0), (10, 10)]) assert_equal(str(m._length.text), '14.1') - assert_equal(str(m._angle.text[:5]), u'135.0') + assert_equal(str(m._angle.text[:5]), '135.0') @skipif(qt_api is None) diff --git a/skimage/viewer/tests/test_viewer.py b/skimage/viewer/tests/test_viewer.py index 00ef848c..75f7413c 100644 --- a/skimage/viewer/tests/test_viewer.py +++ b/skimage/viewer/tests/test_viewer.py @@ -2,7 +2,7 @@ from skimage import data from skimage.viewer import ImageViewer, CollectionViewer from skimage.transform import pyramid_gaussian -from skimage.viewer.plugins import OverlayPlugin +from skimage.viewer.plugins import OverlayPlugin, recent_mpl_version from skimage.filter import sobel from skimage.viewer.qt import qt_api, QtGui, QtCore from numpy.testing import assert_equal @@ -52,7 +52,7 @@ def test_collection_viewer(): view._format_coord(10, 10) -@skipif(qt_api is None) +@skipif(qt_api is None or not recent_mpl_version()) def test_viewer_with_overlay(): img = data.coins() ov = OverlayPlugin(image_filter=sobel) @@ -74,4 +74,4 @@ def test_viewer_with_overlay(): assert_equal(ov.filtered_image, img) - + diff --git a/skimage/viewer/tests/test_widgets.py b/skimage/viewer/tests/test_widgets.py index 61450146..54b9b7bb 100644 --- a/skimage/viewer/tests/test_widgets.py +++ b/skimage/viewer/tests/test_widgets.py @@ -4,7 +4,6 @@ from skimage import data, img_as_float, io from skimage.viewer import ImageViewer from skimage.viewer.widgets import ( Slider, OKCancelButtons, SaveButtons, ComboBox, Text) -from skimage.viewer.utils import init_qtapp from skimage.viewer.plugins.base import Plugin from skimage.viewer.qt import qt_api, QtGui, QtCore From 97d920cc6b3102c3a7c6765568031957f4e1e7d7 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Wed, 16 Jul 2014 21:04:13 -0500 Subject: [PATCH 0157/1122] Another travis fix --- skimage/viewer/tests/test_viewer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/tests/test_viewer.py b/skimage/viewer/tests/test_viewer.py index 75f7413c..f87e49b3 100644 --- a/skimage/viewer/tests/test_viewer.py +++ b/skimage/viewer/tests/test_viewer.py @@ -2,7 +2,8 @@ from skimage import data from skimage.viewer import ImageViewer, CollectionViewer from skimage.transform import pyramid_gaussian -from skimage.viewer.plugins import OverlayPlugin, recent_mpl_version +from skimage.viewer.plugins import OverlayPlugin +from skimage.viewer.plugins.overlayplugin import recent_mpl_version from skimage.filter import sobel from skimage.viewer.qt import qt_api, QtGui, QtCore from numpy.testing import assert_equal From eb4725826d4cbedcd8b375746f0ed8a6da6f9ead Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Wed, 16 Jul 2014 19:57:25 -0700 Subject: [PATCH 0158/1122] Change test of ndimage equivalence to use uint8 test image --- skimage/morphology/tests/test_grey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index ecc0e89e..b7703ec2 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -187,7 +187,7 @@ def test_3d_fallback_black_tophat(): testing.assert_array_equal(new_image, image_expected) def test_2d_ndimage_equivalence(): - image = np.zeros((9, 9), np.uint16) + image = np.zeros((9, 9), np.uint8) image[2:-2, 2:-2] = 2**14 image[3:-3, 3:-3] = 2**15 image[4, 4] = 2**16-1 From 6e8ae37f0726c3e8a71534cd6b61e48cae90d5fd Mon Sep 17 00:00:00 2001 From: Nelson Brown Date: Wed, 16 Jul 2014 20:03:37 -0700 Subject: [PATCH 0159/1122] Change ndimage test image to fit uint8 range --- skimage/morphology/tests/test_grey.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index b7703ec2..83d10b45 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -188,9 +188,9 @@ def test_3d_fallback_black_tophat(): def test_2d_ndimage_equivalence(): image = np.zeros((9, 9), np.uint8) - image[2:-2, 2:-2] = 2**14 - image[3:-3, 3:-3] = 2**15 - image[4, 4] = 2**16-1 + image[2:-2, 2:-2] = 128 + image[3:-3, 3:-3] = 196 + image[4, 4] = 255 opened = grey.opening(image) closed = grey.closing(image) From c24adec714d824dec981283bf66f15357af180a4 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 16 Jul 2014 23:13:04 -0500 Subject: [PATCH 0160/1122] Add `adapt_rgb` decorator and helpers. This applies `adapt_rgb` to `equalize_adapthist` and removes the special-casing of RGB images in that function. Note that tests of `adapt_rgb` fail because some type conversion and intensity scaling were added to pass tests for `equalize_adapthist`. --- skimage/color/adapt_rgb.py | 83 +++++++++++++++++++++++++ skimage/color/tests/test_adapt_rgb.py | 83 +++++++++++++++++++++++++ skimage/exposure/_adapthist.py | 17 +---- skimage/exposure/tests/test_exposure.py | 1 - 4 files changed, 169 insertions(+), 15 deletions(-) create mode 100644 skimage/color/adapt_rgb.py create mode 100644 skimage/color/tests/test_adapt_rgb.py diff --git a/skimage/color/adapt_rgb.py b/skimage/color/adapt_rgb.py new file mode 100644 index 00000000..c35d4087 --- /dev/null +++ b/skimage/color/adapt_rgb.py @@ -0,0 +1,83 @@ +import functools + +import numpy as np + +from skimage import img_as_float +from skimage import color +from skimage.exposure import rescale_intensity +from skimage.util.dtype import convert + + +__all__ = ['adapt_rgb', 'hsv_value', 'each_channel'] + + +def is_rgb_like(image): + """Return True if the image *looks* like it's RGB. + + This function should not be public because it is only intended to be used + for functions that don't accept volumes as input, since checking an image's + shape is fragile. + """ + return (image.ndim == 3) and (image.shape[2] in (3, 4)) + + +def adapt_rgb(apply_to_rgb): + """Return decorator that adapts to RGB images to a gray-scale filter. + + This function is only intended to be used for functions that don't accept + volumes as input, since checking an image's shape is fragile. + + Parameters + ---------- + apply_to_rgb : function + Function that returns a filtered image from an image-filter and RGB + image. This will only be called if the image is RGB-like. + """ + def decorator(image_filter): + # @functools.wraps + def image_filter_adapted(image, *args, **kwargs): + if is_rgb_like(image): + return apply_to_rgb(image_filter, image, *args, **kwargs) + else: + return image_filter(image, *args, **kwargs) + return image_filter_adapted + return decorator + + +def hsv_value(image_filter, image, *args, **kwargs): + """Return color image by applying `image_filter` on HSV-value of `image`. + + Note that this function is intended for use with `adapt_rgb`. + + Parameters + ---------- + image_filter : function + Function that filters a gray-scale image. + image : array + Input image. Note that RGBA images are treated as RGB. + """ + # XXX: Are these 2 lines really necessary? + image = img_as_float(image[:, :, :3]) + image = rescale_intensity(image) + # Slice the first three channels so that we remove any alpha channels. + hsv = color.rgb2hsv(image) + value = hsv[:, :, 2].copy() + value = image_filter(value, *args, **kwargs) + hsv[:, :, 2] = convert(value, hsv.dtype) + return color.hsv2rgb(hsv) + + +def each_channel(image_filter, image, *args, **kwargs): + """Return color image by applying `image_filter` on channels of `image`. + + Note that this function is intended for use with `adapt_rgb`. + + Parameters + ---------- + image_filter : function + Function that filters a gray-scale image. + image : array + Input image. + """ + c_new = [image_filter(c, *args, **kwargs) for c in image.T] + return np.array(c_new).T diff --git a/skimage/color/tests/test_adapt_rgb.py b/skimage/color/tests/test_adapt_rgb.py new file mode 100644 index 00000000..869a1ccc --- /dev/null +++ b/skimage/color/tests/test_adapt_rgb.py @@ -0,0 +1,83 @@ +from functools import partial + +import numpy as np + +from skimage import img_as_float, img_as_uint +from skimage import color, data, filter +from skimage.color.adapt_rgb import adapt_rgb, each_channel, hsv_value + + +# Down-sample image for quicker testing. +COLOR_IMAGE = data.lena()[::5, ::5] +GRAY_IMAGE = data.camera()[::5, ::5] + +SIGMA = 3 +smooth = partial(filter.gaussian_filter, sigma=SIGMA) +assert_allclose = partial(np.testing.assert_allclose, atol=1e-8) + + +@adapt_rgb(each_channel) +def edges_each(image): + return filter.sobel(image) + + +@adapt_rgb(each_channel) +def smooth_each(image, sigma): + return filter.gaussian_filter(image, sigma) + + +@adapt_rgb(hsv_value) +def edges_hsv(image): + return filter.sobel(image) + + +@adapt_rgb(hsv_value) +def smooth_hsv(image, sigma): + return filter.gaussian_filter(image, sigma) + + +@adapt_rgb(hsv_value) +def edges_hsv_uint(image): + return img_as_uint(filter.sobel(image)) + + +def test_gray_scale_image(): + # We don't need to test both `hsv_value` and `each_channel` since + # `adapt_rgb` is handling gray-scale inputs. + assert_allclose(edges_each(GRAY_IMAGE), filter.sobel(GRAY_IMAGE)) + + +def test_each_channel(): + filtered = edges_each(COLOR_IMAGE) + for i, channel in enumerate(np.rollaxis(filtered, axis=-1)): + expected = img_as_float(filter.sobel(COLOR_IMAGE[:, :, i])) + assert_allclose(channel, expected) + + +def test_each_channel_with_filter_argument(): + filtered = smooth_each(COLOR_IMAGE, SIGMA) + for i, channel in enumerate(np.rollaxis(filtered, axis=-1)): + assert_allclose(channel, smooth(COLOR_IMAGE[:, :, i])) + + +def test_hsv_value(): + filtered = edges_hsv(COLOR_IMAGE) + value = color.rgb2hsv(COLOR_IMAGE)[:, :, 2] + assert_allclose(color.rgb2hsv(filtered)[:, :, 2], filter.sobel(value)) + + +def test_hsv_value_with_filter_argument(): + filtered = smooth_hsv(COLOR_IMAGE, SIGMA) + value = color.rgb2hsv(COLOR_IMAGE)[:, :, 2] + assert_allclose(color.rgb2hsv(filtered)[:, :, 2], smooth(value)) + + +def test_hsv_value_with_non_float_output(): + # Since `rgb2hsv` returns a float image and the result of the filtered + # result is inserted into the HSV image, we want to make sure there isn't + # a dtype mismatch. + filtered = edges_hsv_uint(COLOR_IMAGE) + filtered_value = color.rgb2hsv(filtered)[:, :, 2] + value = color.rgb2hsv(COLOR_IMAGE)[:, :, 2] + # Reduce tolerance because dtype conversion. + assert_allclose(filtered_value, filter.sobel(value), rtol=1e-5, atol=1e-5) diff --git a/skimage/exposure/_adapthist.py b/skimage/exposure/_adapthist.py index 39d35b54..6c80dbd9 100644 --- a/skimage/exposure/_adapthist.py +++ b/skimage/exposure/_adapthist.py @@ -16,6 +16,7 @@ comes with no guarantee. import numpy as np import skimage from skimage import color +from skimage.color.adapt_rgb import adapt_rgb, hsv_value from skimage.exposure import rescale_intensity from skimage.util import view_as_blocks @@ -25,6 +26,7 @@ MAX_REG_Y = 16 # max. # contextual regions in y-direction */ NR_OF_GREY = 2**14 # number of grayscale levels to use in CLAHE algorithm +@adapt_rgb(hsv_value) def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, nbins=256): """Contrast Limited Adaptive Histogram Equalization. @@ -65,25 +67,12 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, .. [1] http://tog.acm.org/resources/GraphicsGems/gems.html#gemsvi .. [2] https://en.wikipedia.org/wiki/CLAHE#CLAHE """ - ndim = image.ndim - if ndim == 3: - if image.shape[2] == 4: - image = image[:, :, :3] - image = skimage.img_as_float(image) - image = rescale_intensity(image) - hsv_img = color.rgb2hsv(image) - image = hsv_img[:, :, 2].copy() image = skimage.img_as_uint(image) image = rescale_intensity(image, out_range=(0, NR_OF_GREY - 1)) out = _clahe(image, ntiles_x, ntiles_y, clip_limit * nbins, nbins) image[:out.shape[0], :out.shape[1]] = out image = skimage.img_as_float(image) - if ndim == 3: - hsv_img[:, :, 2] = rescale_intensity(image) - image = color.hsv2rgb(hsv_img) - else: - image = rescale_intensity(image) - return image + return rescale_intensity(image) def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py index c793c2c8..6c3c2a4c 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -11,7 +11,6 @@ from skimage.exposure.exposure import intensity_range from skimage.color import rgb2gray from skimage.util.dtype import dtype_range -import matplotlib.pyplot as plt # Test histogram equalization # =========================== From d9a39d44eb03dcb647c3e71dff3cc7f755a9d839 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 16 Jul 2014 23:24:03 -0500 Subject: [PATCH 0161/1122] Remove cast and rescale and adjust test values to match --- skimage/color/adapt_rgb.py | 5 +--- skimage/exposure/tests/test_exposure.py | 35 ++++++++++++------------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/skimage/color/adapt_rgb.py b/skimage/color/adapt_rgb.py index c35d4087..6959de79 100644 --- a/skimage/color/adapt_rgb.py +++ b/skimage/color/adapt_rgb.py @@ -56,11 +56,8 @@ def hsv_value(image_filter, image, *args, **kwargs): image : array Input image. Note that RGBA images are treated as RGB. """ - # XXX: Are these 2 lines really necessary? - image = img_as_float(image[:, :, :3]) - image = rescale_intensity(image) # Slice the first three channels so that we remove any alpha channels. - hsv = color.rgb2hsv(image) + hsv = color.rgb2hsv(image[:, :, :3]) value = hsv[:, :, 2].copy() value = image_filter(value, *args, **kwargs) hsv[:, :, 2] = convert(value, hsv.dtype) diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py index 6c3c2a4c..72a4e413 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -145,8 +145,8 @@ def test_rescale_uint14_limits(): # ==================================== def test_adapthist_scalar(): - '''Test a scalar uint8 image - ''' + """Test a scalar uint8 image + """ img = skimage.img_as_ubyte(data.moon()) adapted = exposure.equalize_adapthist(img, clip_limit=0.02) assert adapted.min() == 0.0 @@ -162,8 +162,8 @@ def test_adapthist_scalar(): def test_adapthist_grayscale(): - '''Test a grayscale float image - ''' + """Test a grayscale float image + """ img = skimage.img_as_float(data.lena()) img = rgb2gray(img) img = np.dstack((img, img, img)) @@ -171,14 +171,14 @@ def test_adapthist_grayscale(): nbins=128) assert_almost_equal = np.testing.assert_almost_equal assert img.shape == adapted.shape - assert_almost_equal(peak_snr(img, adapted), 104.307, 3) + assert_almost_equal(peak_snr(img, adapted), 104.3277, 3) assert_almost_equal(norm_brightness_err(img, adapted), 0.0265, 3) return data, adapted def test_adapthist_color(): - '''Test an RGB color uint16 image - ''' + """Test an RGB color uint16 image + """ img = skimage.img_as_uint(data.lena()) with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') @@ -191,15 +191,14 @@ def test_adapthist_color(): assert adapted.max() == 1.0 assert img.shape == adapted.shape full_scale = skimage.exposure.rescale_intensity(img) - assert_almost_equal(peak_snr(full_scale, adapted), 105.50517, 3) - assert_almost_equal(norm_brightness_err(full_scale, adapted), - 0.0544, 3) + assert_almost_equal(peak_snr(full_scale, adapted), 106.9, 1) + assert_almost_equal(norm_brightness_err(full_scale, adapted), 0.05, 2) return data, adapted def test_adapthist_alpha(): - '''Test an RGBA color image - ''' + """Test an RGBA color image + """ img = skimage.img_as_float(data.lena()) alpha = np.ones((img.shape[0], img.shape[1]), dtype=float) img = np.dstack((img, alpha)) @@ -209,12 +208,12 @@ def test_adapthist_alpha(): full_scale = skimage.exposure.rescale_intensity(img) assert img.shape == adapted.shape assert_almost_equal = np.testing.assert_almost_equal - assert_almost_equal(peak_snr(full_scale, adapted), 105.50198, 3) - assert_almost_equal(norm_brightness_err(full_scale, adapted), 0.0544, 3) + assert_almost_equal(peak_snr(full_scale, adapted), 106.86, 2) + assert_almost_equal(norm_brightness_err(full_scale, adapted), 0.0509, 3) def peak_snr(img1, img2): - '''Peak signal to noise ratio of two images + """Peak signal to noise ratio of two images Parameters ---------- @@ -225,7 +224,7 @@ def peak_snr(img1, img2): ------- peak_snr : float Peak signal to noise ratio - ''' + """ if img1.ndim == 3: img1, img2 = rgb2gray(img1.copy()), rgb2gray(img2.copy()) img1 = skimage.img_as_float(img1) @@ -236,7 +235,7 @@ def peak_snr(img1, img2): def norm_brightness_err(img1, img2): - '''Normalized Absolute Mean Brightness Error between two images + """Normalized Absolute Mean Brightness Error between two images Parameters ---------- @@ -247,7 +246,7 @@ def norm_brightness_err(img1, img2): ------- norm_brightness_error : float Normalized absolute mean brightness error - ''' + """ if img1.ndim == 3: img1, img2 = rgb2gray(img1), rgb2gray(img2) ambe = np.abs(img1.mean() - img2.mean()) From 34d7fdb1382aff7ac9b9ee3126a2b0f8c29e6998 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 16 Jul 2014 23:45:55 -0500 Subject: [PATCH 0162/1122] Fix docstring for wrapped functions --- skimage/color/adapt_rgb.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/skimage/color/adapt_rgb.py b/skimage/color/adapt_rgb.py index 6959de79..d5966813 100644 --- a/skimage/color/adapt_rgb.py +++ b/skimage/color/adapt_rgb.py @@ -2,9 +2,7 @@ import functools import numpy as np -from skimage import img_as_float from skimage import color -from skimage.exposure import rescale_intensity from skimage.util.dtype import convert @@ -34,7 +32,7 @@ def adapt_rgb(apply_to_rgb): image. This will only be called if the image is RGB-like. """ def decorator(image_filter): - # @functools.wraps + @functools.wraps(image_filter) def image_filter_adapted(image, *args, **kwargs): if is_rgb_like(image): return apply_to_rgb(image_filter, image, *args, **kwargs) From acbac35616779d6f625f7ed706d55a64b4c67b6c Mon Sep 17 00:00:00 2001 From: blink1073 Date: Thu, 17 Jul 2014 12:33:59 -0500 Subject: [PATCH 0163/1122] Skip tests if matplotlib is not present --- skimage/viewer/tests/test_plugins.py | 40 ++++++++++++++++------------ skimage/viewer/tests/test_tools.py | 26 +++++++++++------- skimage/viewer/tests/test_utils.py | 13 ++++++--- skimage/viewer/tests/test_viewer.py | 20 +++++++++----- skimage/viewer/tests/test_widgets.py | 29 +++++++++++--------- 5 files changed, 79 insertions(+), 49 deletions(-) diff --git a/skimage/viewer/tests/test_plugins.py b/skimage/viewer/tests/test_plugins.py index 59b1f3e4..2f4d5ef5 100644 --- a/skimage/viewer/tests/test_plugins.py +++ b/skimage/viewer/tests/test_plugins.py @@ -4,15 +4,21 @@ import skimage import skimage.data as data from skimage.filter.rank import median from skimage.morphology import disk -from skimage.viewer import ImageViewer -from skimage.viewer.qt import qt_api from numpy.testing import assert_equal, assert_allclose, assert_almost_equal from numpy.testing.decorators import skipif -from skimage.viewer.plugins import ( - LineProfile, Measure, CannyPlugin, LabelPainter, Crop, ColorHistogram, - PlotPlugin) -from skimage.viewer.plugins.base import Plugin -from skimage.viewer.widgets import Slider + +try: + from skimage.viewer.qt import qt_api + from skimage.viewer import ImageViewer + from skimage.viewer.plugins import ( + LineProfile, Measure, CannyPlugin, LabelPainter, Crop, ColorHistogram, + PlotPlugin) + from skimage.viewer.plugins.base import Plugin + from skimage.viewer.widgets import Slider +except ImportError: + skip_all = True +else: + skip_all = False def setup_line_profile(image, limits='image'): @@ -22,7 +28,7 @@ def setup_line_profile(image, limits='image'): return plugin -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_line_profile(): """ Test a line profile using an ndim=2 image""" plugin = setup_line_profile(data.camera()) @@ -36,7 +42,7 @@ def test_line_profile(): assert_allclose(scan_data.mean(), 0.2812, rtol=1e-3) -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_line_profile_rgb(): """ Test a line profile using an ndim=3 image""" plugin = setup_line_profile(data.chelsea(), limits=None) @@ -51,7 +57,7 @@ def test_line_profile_rgb(): assert_allclose(scan_data.mean(), 0.4359, rtol=1e-3) -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_line_profile_dynamic(): """Test a line profile updating after an image transform""" image = data.coins()[:-50, :] # shave some off to make the line lower @@ -76,7 +82,7 @@ def test_line_profile_dynamic(): assert_almost_equal(np.max(line) - np.min(line), 0.639, 1) -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_measure(): image = data.camera() viewer = ImageViewer(image) @@ -88,7 +94,7 @@ def test_measure(): assert_equal(str(m._angle.text[:5]), '135.0') -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_canny(): image = data.camera() viewer = ImageViewer(image) @@ -101,7 +107,7 @@ def test_canny(): assert edges.sum() == 2852 -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_label_painter(): image = data.camera() moon = data.moon() @@ -119,7 +125,7 @@ def test_label_painter(): assert_equal(lp.paint_tool.shape, moon.shape) -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_crop(): image = data.camera() viewer = ImageViewer(image) @@ -130,7 +136,7 @@ def test_crop(): assert_equal(viewer.image.shape, (101, 101)) -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_color_histogram(): image = skimage.img_as_float(data.load('color.png')) viewer = ImageViewer(image) @@ -142,7 +148,7 @@ def test_color_histogram(): assert_almost_equal(viewer.image.std(), 0.325, 3) -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_plot_plugin(): viewer = ImageViewer(data.moon()) plugin = PlotPlugin(image_filter=lambda x: x) @@ -154,7 +160,7 @@ def test_plot_plugin(): viewer.close() -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_plugin(): img = skimage.img_as_float(data.moon()) viewer = ImageViewer(img) diff --git a/skimage/viewer/tests/test_tools.py b/skimage/viewer/tests/test_tools.py index 853dd3d8..8cffd323 100644 --- a/skimage/viewer/tests/test_tools.py +++ b/skimage/viewer/tests/test_tools.py @@ -2,14 +2,20 @@ from collections import namedtuple import numpy as np from skimage import data -from skimage.viewer import ImageViewer -from skimage.viewer.canvastools import ( - LineTool, ThickLineTool, RectangleTool, PaintTool) -from skimage.viewer.canvastools.base import CanvasToolBase -from skimage.viewer.qt import qt_api from numpy.testing import assert_equal from numpy.testing.decorators import skipif +try: + from skimage.viewer.qt import qt_api + from skimage.viewer import ImageViewer + from skimage.viewer.canvastools import ( + LineTool, ThickLineTool, RectangleTool, PaintTool) + from skimage.viewer.canvastools.base import CanvasToolBase +except ImportError: + skip_all = True +else: + skip_all = False + def get_end_points(image): h, w = image.shape[0:2] @@ -70,7 +76,7 @@ def create_mouse_event(ax, button=1, xdata=0, ydata=0, key=None): return event -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_line_tool(): img = data.camera() viewer = ImageViewer(img) @@ -96,7 +102,7 @@ def test_line_tool(): assert_equal(tool.geometry, np.array([[100, 100], [10, 10]])) -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_thick_line_tool(): img = data.camera() viewer = ImageViewer(img) @@ -121,7 +127,7 @@ def test_thick_line_tool(): assert_equal(tool.linewidth, 1) -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_rect_tool(): img = data.camera() viewer = ImageViewer(img) @@ -153,7 +159,7 @@ def test_rect_tool(): assert_equal(tool.geometry, [10, 100, 10, 100]) -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_paint_tool(): img = data.moon() viewer = ImageViewer(img) @@ -189,7 +195,7 @@ def test_paint_tool(): assert_equal(tool.overlay.sum(), 0) -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_base_tool(): img = data.moon() viewer = ImageViewer(img) diff --git a/skimage/viewer/tests/test_utils.py b/skimage/viewer/tests/test_utils.py index b72d99ab..30d74a51 100644 --- a/skimage/viewer/tests/test_utils.py +++ b/skimage/viewer/tests/test_utils.py @@ -1,8 +1,12 @@ # -*- coding: utf-8 -*- -from skimage.viewer.qt import qt_api, QtCore, QtGui from numpy.testing.decorators import skipif -from skimage.viewer import utils -from skimage.viewer.utils import dialogs + +try: + from skimage.viewer.qt import qt_api, QtCore, QtGui + from skimage.viewer import utils + from skimage.viewer.utils import dialogs +except ImportError: + qt_api = None @skipif(qt_api is None) @@ -13,6 +17,7 @@ def test_event_loop(): utils.start_qtapp() +@skipif(qt_api is None) def test_format_filename(): fname = dialogs._format_filename(('apple', 2)) assert fname == 'apple' @@ -20,6 +25,7 @@ def test_format_filename(): assert fname is None +@skipif(qt_api is None) def test_open_file_dialog(): utils.init_qtapp() timer = QtCore.QTimer() @@ -28,6 +34,7 @@ def test_open_file_dialog(): assert filename is None +@skipif(qt_api is None) def test_save_file_dialog(): utils.init_qtapp() timer = QtCore.QTimer() diff --git a/skimage/viewer/tests/test_viewer.py b/skimage/viewer/tests/test_viewer.py index f87e49b3..2a232b29 100644 --- a/skimage/viewer/tests/test_viewer.py +++ b/skimage/viewer/tests/test_viewer.py @@ -1,16 +1,22 @@ from skimage import data -from skimage.viewer import ImageViewer, CollectionViewer from skimage.transform import pyramid_gaussian -from skimage.viewer.plugins import OverlayPlugin -from skimage.viewer.plugins.overlayplugin import recent_mpl_version from skimage.filter import sobel -from skimage.viewer.qt import qt_api, QtGui, QtCore from numpy.testing import assert_equal from numpy.testing.decorators import skipif +try: + from skimage.viewer.qt import qt_api, QtGui, QtCore + from skimage.viewer.plugins import OverlayPlugin + from skimage.viewer.plugins.overlayplugin import recent_mpl_version + from skimage.viewer import ImageViewer, CollectionViewer +except ImportError: + skip_all = True +else: + skip_all = False -@skipif(qt_api is None) + +@skipif(skip_all or qt_api is None) def test_viewer(): lena = data.lena() coins = data.coins() @@ -37,7 +43,7 @@ def make_key_event(key): QtCore.Qt.NoModifier) -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_collection_viewer(): img = data.lena() @@ -53,7 +59,7 @@ def test_collection_viewer(): view._format_coord(10, 10) -@skipif(qt_api is None or not recent_mpl_version()) +@skipif(skip_all or qt_api is None or not recent_mpl_version()) def test_viewer_with_overlay(): img = data.coins() ov = OverlayPlugin(image_filter=sobel) diff --git a/skimage/viewer/tests/test_widgets.py b/skimage/viewer/tests/test_widgets.py index 54b9b7bb..3a29c4ba 100644 --- a/skimage/viewer/tests/test_widgets.py +++ b/skimage/viewer/tests/test_widgets.py @@ -1,15 +1,20 @@ import os from skimage import data, img_as_float, io -from skimage.viewer import ImageViewer -from skimage.viewer.widgets import ( - Slider, OKCancelButtons, SaveButtons, ComboBox, Text) -from skimage.viewer.plugins.base import Plugin - -from skimage.viewer.qt import qt_api, QtGui, QtCore from numpy.testing import assert_almost_equal, assert_equal from numpy.testing.decorators import skipif +try: + from skimage.viewer.qt import qt_api, QtGui, QtCore + from skimage.viewer import ImageViewer + from skimage.viewer.widgets import ( + Slider, OKCancelButtons, SaveButtons, ComboBox, Text) + from skimage.viewer.plugins.base import Plugin +except ImportError: + skip_all = True +else: + skip_all = False + def get_image_viewer(): image = data.coins() @@ -18,7 +23,7 @@ def get_image_viewer(): return viewer -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_combo_box(): viewer = get_image_viewer() cb = ComboBox('hello', ('a', 'b', 'c')) @@ -31,7 +36,7 @@ def test_combo_box(): assert_equal(cb.index, 2) -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_text_widget(): viewer = get_image_viewer() txt = Text('hello', 'hello, world!') @@ -42,7 +47,7 @@ def test_text_widget(): assert_equal(str(txt.text), 'goodbye, world!') -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_slider_int(): viewer = get_image_viewer() sld = Slider('radius', 2, 10, value_type='int') @@ -56,7 +61,7 @@ def test_slider_int(): assert_equal(sld.val, 5) -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_slider_float(): viewer = get_image_viewer() sld = Slider('alpha', 2.1, 3.1, value=2.1, value_type='float', @@ -71,7 +76,7 @@ def test_slider_float(): assert_almost_equal(sld.val, 2.5, 2) -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_save_buttons(): viewer = get_image_viewer() sv = SaveButtons() @@ -94,7 +99,7 @@ def test_save_buttons(): assert_almost_equal(img, viewer.image) -@skipif(qt_api is None) +@skipif(skip_all or qt_api is None) def test_ok_buttons(): viewer = get_image_viewer() ok = OKCancelButtons() From 243d5505370d7405fa32a04bdac1a52cb689e678 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Thu, 17 Jul 2014 19:52:45 -0500 Subject: [PATCH 0164/1122] Make skip condition cleaner --- skimage/viewer/tests/test_plugins.py | 25 ++++++++++++------------- skimage/viewer/tests/test_tools.py | 15 +++++++-------- skimage/viewer/tests/test_viewer.py | 11 +++++------ skimage/viewer/tests/test_widgets.py | 17 ++++++++--------- 4 files changed, 32 insertions(+), 36 deletions(-) diff --git a/skimage/viewer/tests/test_plugins.py b/skimage/viewer/tests/test_plugins.py index 2f4d5ef5..7ef62730 100644 --- a/skimage/viewer/tests/test_plugins.py +++ b/skimage/viewer/tests/test_plugins.py @@ -15,10 +15,9 @@ try: PlotPlugin) from skimage.viewer.plugins.base import Plugin from skimage.viewer.widgets import Slider + viewer_available = not qt_api is None except ImportError: - skip_all = True -else: - skip_all = False + viewer_available = False def setup_line_profile(image, limits='image'): @@ -28,7 +27,7 @@ def setup_line_profile(image, limits='image'): return plugin -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_line_profile(): """ Test a line profile using an ndim=2 image""" plugin = setup_line_profile(data.camera()) @@ -42,7 +41,7 @@ def test_line_profile(): assert_allclose(scan_data.mean(), 0.2812, rtol=1e-3) -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_line_profile_rgb(): """ Test a line profile using an ndim=3 image""" plugin = setup_line_profile(data.chelsea(), limits=None) @@ -57,7 +56,7 @@ def test_line_profile_rgb(): assert_allclose(scan_data.mean(), 0.4359, rtol=1e-3) -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_line_profile_dynamic(): """Test a line profile updating after an image transform""" image = data.coins()[:-50, :] # shave some off to make the line lower @@ -82,7 +81,7 @@ def test_line_profile_dynamic(): assert_almost_equal(np.max(line) - np.min(line), 0.639, 1) -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_measure(): image = data.camera() viewer = ImageViewer(image) @@ -94,7 +93,7 @@ def test_measure(): assert_equal(str(m._angle.text[:5]), '135.0') -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_canny(): image = data.camera() viewer = ImageViewer(image) @@ -107,7 +106,7 @@ def test_canny(): assert edges.sum() == 2852 -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_label_painter(): image = data.camera() moon = data.moon() @@ -125,7 +124,7 @@ def test_label_painter(): assert_equal(lp.paint_tool.shape, moon.shape) -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_crop(): image = data.camera() viewer = ImageViewer(image) @@ -136,7 +135,7 @@ def test_crop(): assert_equal(viewer.image.shape, (101, 101)) -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_color_histogram(): image = skimage.img_as_float(data.load('color.png')) viewer = ImageViewer(image) @@ -148,7 +147,7 @@ def test_color_histogram(): assert_almost_equal(viewer.image.std(), 0.325, 3) -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_plot_plugin(): viewer = ImageViewer(data.moon()) plugin = PlotPlugin(image_filter=lambda x: x) @@ -160,7 +159,7 @@ def test_plot_plugin(): viewer.close() -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_plugin(): img = skimage.img_as_float(data.moon()) viewer = ImageViewer(img) diff --git a/skimage/viewer/tests/test_tools.py b/skimage/viewer/tests/test_tools.py index 8cffd323..9f02e762 100644 --- a/skimage/viewer/tests/test_tools.py +++ b/skimage/viewer/tests/test_tools.py @@ -11,10 +11,9 @@ try: from skimage.viewer.canvastools import ( LineTool, ThickLineTool, RectangleTool, PaintTool) from skimage.viewer.canvastools.base import CanvasToolBase + viewer_available = not qt_api is None except ImportError: - skip_all = True -else: - skip_all = False + viewer_available = False def get_end_points(image): @@ -76,7 +75,7 @@ def create_mouse_event(ax, button=1, xdata=0, ydata=0, key=None): return event -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_line_tool(): img = data.camera() viewer = ImageViewer(img) @@ -102,7 +101,7 @@ def test_line_tool(): assert_equal(tool.geometry, np.array([[100, 100], [10, 10]])) -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_thick_line_tool(): img = data.camera() viewer = ImageViewer(img) @@ -127,7 +126,7 @@ def test_thick_line_tool(): assert_equal(tool.linewidth, 1) -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_rect_tool(): img = data.camera() viewer = ImageViewer(img) @@ -159,7 +158,7 @@ def test_rect_tool(): assert_equal(tool.geometry, [10, 100, 10, 100]) -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_paint_tool(): img = data.moon() viewer = ImageViewer(img) @@ -195,7 +194,7 @@ def test_paint_tool(): assert_equal(tool.overlay.sum(), 0) -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_base_tool(): img = data.moon() viewer = ImageViewer(img) diff --git a/skimage/viewer/tests/test_viewer.py b/skimage/viewer/tests/test_viewer.py index 2a232b29..cd6c7acb 100644 --- a/skimage/viewer/tests/test_viewer.py +++ b/skimage/viewer/tests/test_viewer.py @@ -10,13 +10,12 @@ try: from skimage.viewer.plugins import OverlayPlugin from skimage.viewer.plugins.overlayplugin import recent_mpl_version from skimage.viewer import ImageViewer, CollectionViewer + viewer_available = not qt_api is None except ImportError: - skip_all = True -else: - skip_all = False + viewer_available = False -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_viewer(): lena = data.lena() coins = data.coins() @@ -43,7 +42,7 @@ def make_key_event(key): QtCore.Qt.NoModifier) -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_collection_viewer(): img = data.lena() @@ -59,7 +58,7 @@ def test_collection_viewer(): view._format_coord(10, 10) -@skipif(skip_all or qt_api is None or not recent_mpl_version()) +@skipif(not viewer_available or not recent_mpl_version()) def test_viewer_with_overlay(): img = data.coins() ov = OverlayPlugin(image_filter=sobel) diff --git a/skimage/viewer/tests/test_widgets.py b/skimage/viewer/tests/test_widgets.py index 3a29c4ba..aeb4ede2 100644 --- a/skimage/viewer/tests/test_widgets.py +++ b/skimage/viewer/tests/test_widgets.py @@ -10,10 +10,9 @@ try: from skimage.viewer.widgets import ( Slider, OKCancelButtons, SaveButtons, ComboBox, Text) from skimage.viewer.plugins.base import Plugin + viewer_available = not qt_api is None except ImportError: - skip_all = True -else: - skip_all = False + viewer_available = False def get_image_viewer(): @@ -23,7 +22,7 @@ def get_image_viewer(): return viewer -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_combo_box(): viewer = get_image_viewer() cb = ComboBox('hello', ('a', 'b', 'c')) @@ -36,7 +35,7 @@ def test_combo_box(): assert_equal(cb.index, 2) -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_text_widget(): viewer = get_image_viewer() txt = Text('hello', 'hello, world!') @@ -47,7 +46,7 @@ def test_text_widget(): assert_equal(str(txt.text), 'goodbye, world!') -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_slider_int(): viewer = get_image_viewer() sld = Slider('radius', 2, 10, value_type='int') @@ -61,7 +60,7 @@ def test_slider_int(): assert_equal(sld.val, 5) -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_slider_float(): viewer = get_image_viewer() sld = Slider('alpha', 2.1, 3.1, value=2.1, value_type='float', @@ -76,7 +75,7 @@ def test_slider_float(): assert_almost_equal(sld.val, 2.5, 2) -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_save_buttons(): viewer = get_image_viewer() sv = SaveButtons() @@ -99,7 +98,7 @@ def test_save_buttons(): assert_almost_equal(img, viewer.image) -@skipif(skip_all or qt_api is None) +@skipif(not viewer_available) def test_ok_buttons(): viewer = get_image_viewer() ok = OKCancelButtons() From ee3b9f2d66c2583a8148e5214336fa211ddd6913 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 17 Jul 2014 20:50:39 -0500 Subject: [PATCH 0165/1122] Remove orphaned import --- skimage/exposure/_adapthist.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skimage/exposure/_adapthist.py b/skimage/exposure/_adapthist.py index 6c80dbd9..5da50029 100644 --- a/skimage/exposure/_adapthist.py +++ b/skimage/exposure/_adapthist.py @@ -15,7 +15,6 @@ comes with no guarantee. """ import numpy as np import skimage -from skimage import color from skimage.color.adapt_rgb import adapt_rgb, hsv_value from skimage.exposure import rescale_intensity from skimage.util import view_as_blocks From 2a1f368bcfcb91f9a945bcd4aef87a8570efc5e8 Mon Sep 17 00:00:00 2001 From: Alexey Buzmakov Date: Sat, 19 Jul 2014 13:48:40 +0400 Subject: [PATCH 0166/1122] Remove endpoints from theta MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding endpoint to theta make iradon (FBP) reсonstraction worse (adding 2 vertical lines at the edge of phantom). This is misleading, that the iradon (FBP) is much worse than iradon_sart (SART) , and it is not. --- doc/examples/plot_radon_transform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/examples/plot_radon_transform.py b/doc/examples/plot_radon_transform.py index 04cf7bab..98344e8e 100644 --- a/doc/examples/plot_radon_transform.py +++ b/doc/examples/plot_radon_transform.py @@ -65,7 +65,7 @@ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4.5)) ax1.set_title("Original") ax1.imshow(image, cmap=plt.cm.Greys_r) -theta = np.linspace(0., 180., max(image.shape), endpoint=True) +theta = np.linspace(0., 180., max(image.shape), endpoint=False) sinogram = radon(image, theta=theta, circle=True) ax2.set_title("Radon transform\n(Sinogram)") ax2.set_xlabel("Projection angle (deg)") From 5677dafe414eca075afb082540ae161248663bde Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sat, 19 Jul 2014 18:18:23 -0500 Subject: [PATCH 0167/1122] Move away from random.random in favor of random.rand --- skimage/color/tests/test_colorconv.py | 4 ++-- skimage/feature/brief.py | 4 ++-- skimage/feature/tests/test_censure.py | 4 ++-- skimage/feature/tests/test_peak.py | 9 ++++----- skimage/feature/tests/test_texture.py | 2 +- skimage/filter/_canny.py | 2 +- skimage/filter/rank/tests/test_rank.py | 6 +++--- skimage/graph/tests/test_mcp.py | 10 +++++----- skimage/io/_io.py | 2 +- skimage/io/tests/test_freeimage.py | 4 ++-- skimage/io/tests/test_imread.py | 2 +- skimage/io/tests/test_pil.py | 2 +- skimage/io/tests/test_plugin_util.py | 12 ++++++------ skimage/io/tests/test_simpleitk.py | 2 +- skimage/io/tests/test_tifffile.py | 2 +- skimage/measure/fit.py | 3 ++- .../measure/tests/test_structural_similarity.py | 16 ++++++++-------- skimage/morphology/tests/test_ccomp.py | 2 +- skimage/restoration/tests/test_denoise.py | 14 +++++++------- .../segmentation/random_walker_segmentation.py | 5 +++-- skimage/transform/tests/test_integral.py | 4 ++-- skimage/transform/tests/test_warps.py | 2 +- 22 files changed, 57 insertions(+), 56 deletions(-) diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index bbc1c61b..63336a76 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -225,7 +225,7 @@ class TestColorconv(TestCase): assert_equal(g.shape, (1, 1)) def test_rgb2grey_on_grey(self): - rgb2grey(np.random.random((5, 5))) + rgb2grey(np.random.rand(5, 5)) # test matrices for xyz2lab and lab2xyz generated using http://www.easyrgb.com/index.php?X=CALC # Note: easyrgb website displays xyz*100 @@ -351,7 +351,7 @@ def test_gray2rgb(): def test_gray2rgb_rgb(): - x = np.random.random((5, 5, 4)) + x = np.random.rand(5, 5, 4) y = gray2rgb(x) assert_equal(x, y) diff --git a/skimage/feature/brief.py b/skimage/feature/brief.py index d1626f17..cc926113 100644 --- a/skimage/feature/brief.py +++ b/skimage/feature/brief.py @@ -71,7 +71,7 @@ class BRIEF(DescriptorExtractor): [0, 0, 1, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32) + [0, 0, 0, 0, 0, 0, 0, 0]], dtype=np.int32) >>> square2 = np.zeros((9, 9), dtype=np.int32) >>> square2[2:7, 2:7] = 1 >>> square2 @@ -83,7 +83,7 @@ class BRIEF(DescriptorExtractor): [0, 0, 1, 1, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32) + [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=np.int32) >>> keypoints1 = corner_peaks(corner_harris(square1), min_distance=1) >>> keypoints2 = corner_peaks(corner_harris(square2), min_distance=1) >>> extractor = BRIEF(patch_size=5) diff --git a/skimage/feature/tests/test_censure.py b/skimage/feature/tests/test_censure.py index 6a4aed9c..95bfcda3 100644 --- a/skimage/feature/tests/test_censure.py +++ b/skimage/feature/tests/test_censure.py @@ -10,8 +10,8 @@ np.random.seed(0) def test_censure_on_rectangular_images(): """Censure feature detector should work on 2D image of any shape.""" - rect_image = np.random.random((300, 200)) - square_image = np.random.random((200, 200)) + rect_image = np.random.rand(300, 200) + square_image = np.random.rand(200, 200) CENSURE().detect((square_image)) CENSURE().detect((rect_image)) diff --git a/skimage/feature/tests/test_peak.py b/skimage/feature/tests/test_peak.py index 98242a4d..ec130c66 100644 --- a/skimage/feature/tests/test_peak.py +++ b/skimage/feature/tests/test_peak.py @@ -5,6 +5,9 @@ import scipy.ndimage from skimage.feature import peak +np.random.seed(21) + + def test_trivial_case(): trivial = np.zeros((25, 25)) peak_indices = peak.peak_local_max(trivial, min_distance=1, indices=True) @@ -17,7 +20,7 @@ def test_noisy_peaks(): peak_locations = [(7, 7), (7, 13), (13, 7), (13, 13)] # image with noise of amplitude 0.8 and peaks of amplitude 1 - image = 0.8 * np.random.random((20, 20)) + image = 0.8 * np.random.rand(20, 20) for r, c in peak_locations: image[r, c] = 1 @@ -80,7 +83,6 @@ def test_num_peaks(): def test_reorder_labels(): - np.random.seed(21) image = np.random.uniform(size=(40, 60)) i, j = np.mgrid[0:40, 0:60] labels = 1 + (i >= 20) + (j >= 30) * 2 @@ -100,7 +102,6 @@ def test_reorder_labels(): def test_indices_with_labels(): - np.random.seed(21) image = np.random.uniform(size=(40, 60)) i, j = np.mgrid[0:40, 0:60] labels = 1 + (i >= 20) + (j >= 30) * 2 @@ -233,7 +234,6 @@ def test_adjacent_different_objects(): def test_four_quadrants(): - np.random.seed(21) image = np.random.uniform(size=(40, 60)) i, j = np.mgrid[0:40, 0:60] labels = 1 + (i >= 20) + (j >= 30) * 2 @@ -255,7 +255,6 @@ def test_disk(): '''regression test of img-1194, footprint = [1] Test peak.peak_local_max when every point is a local maximum ''' - np.random.seed(31) image = np.random.uniform(size=(10, 20)) footprint = np.array([[1]]) result = peak.peak_local_max(image, labels=np.ones((10, 20)), diff --git a/skimage/feature/tests/test_texture.py b/skimage/feature/tests/test_texture.py index dac511d4..788ccbc6 100644 --- a/skimage/feature/tests/test_texture.py +++ b/skimage/feature/tests/test_texture.py @@ -199,7 +199,7 @@ class TestLBP(): np.random.seed(13141516) # Create random image with known variance. - image = np.random.random((500, 500)) + image = np.random.rand(500, 500) target_std = 0.3 image = image / image.std() * target_std diff --git a/skimage/filter/_canny.py b/skimage/filter/_canny.py index 185bfd40..8dcd1570 100644 --- a/skimage/filter/_canny.py +++ b/skimage/filter/_canny.py @@ -112,7 +112,7 @@ def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None): >>> # Generate noisy image of a square >>> im = np.zeros((256, 256)) >>> im[64:-64, 64:-64] = 1 - >>> im += 0.2 * np.random.random(im.shape) + >>> im += 0.2 * np.random.rand(*im.shape) >>> # First trial with the Canny filter, with the default smoothing >>> edges1 = filter.canny(im) >>> # Increase the smoothing for better results diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py index d8122ba7..5cbffd90 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filter/rank/tests/test_rank.py @@ -46,7 +46,7 @@ def test_random_sizes(): def test_compare_with_cmorph_dilate(): # compare the result of maximum filter with dilate - image = (np.random.random((100, 100)) * 256).astype(np.uint8) + image = (np.random.rand(100, 100) * 256).astype(np.uint8) out = np.empty_like(image) mask = np.ones(image.shape, dtype=np.uint8) @@ -60,7 +60,7 @@ def test_compare_with_cmorph_dilate(): def test_compare_with_cmorph_erode(): # compare the result of maximum filter with erode - image = (np.random.random((100, 100)) * 256).astype(np.uint8) + image = (np.random.rand(100, 100) * 256).astype(np.uint8) out = np.empty_like(image) mask = np.ones(image.shape, dtype=np.uint8) @@ -145,7 +145,7 @@ def test_inplace_output(): # rank filters are not supposed to filter inplace selem = disk(20) - image = (np.random.random((500, 500)) * 256).astype(np.uint8) + image = (np.random.rand(500, 500) * 256).astype(np.uint8) out = image assert_raises(NotImplementedError, rank.mean, image, selem, out=out) diff --git a/skimage/graph/tests/test_mcp.py b/skimage/graph/tests/test_mcp.py index 5c44abad..4dc69858 100644 --- a/skimage/graph/tests/test_mcp.py +++ b/skimage/graph/tests/test_mcp.py @@ -5,6 +5,7 @@ from numpy.testing import (assert_array_equal, import skimage.graph.mcp as mcp +np.random.seed(0) a = np.ones((8, 8), dtype=np.float32) a[1:-1, 1] = 0 a[1, 1:-1] = 0 @@ -133,15 +134,14 @@ def test_crashing(): def _test_random(shape): # Just tests for crashing -- not for correctness. - np.random.seed(0) - a = np.random.random(shape).astype(np.float32) + a = np.random.rand(*shape).astype(np.float32) starts = [[0] * len(shape), [-1] * len(shape), - (np.random.random(len(shape)) * shape).astype(int)] - ends = [(np.random.random(len(shape)) * shape).astype(int) + (np.random.rand(len(shape)) * shape).astype(int)] + ends = [(np.random.rand(len(shape)) * shape).astype(int) for i in range(4)] m = mcp.MCP(a, fully_connected=True) costs, offsets = m.find_costs(starts) - for point in [(np.random.random(len(shape)) * shape).astype(int) + for point in [(np.random.rand(len(shape)) * shape).astype(int) for i in range(4)]: m.traceback(point) m._reset() diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 97f8718e..06da7adb 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -193,7 +193,7 @@ def show(): >>> import skimage.io as io >>> for i in range(4): - ... io.imshow(np.random.random((50, 50))) + ... io.imshow(np.random.rand(50, 50)) >>> io.show() # doctest: +SKIP ''' diff --git a/skimage/io/tests/test_freeimage.py b/skimage/io/tests/test_freeimage.py index dd34828f..a2d88db3 100644 --- a/skimage/io/tests/test_freeimage.py +++ b/skimage/io/tests/test_freeimage.py @@ -64,7 +64,7 @@ def test_imread_uint16_big_endian(): @skipif(not FI_available) def test_write_multipage(): shape = (64, 64, 64) - x = np.ones(shape, dtype=np.uint8) * np.random.random(shape) * 255 + x = np.ones(shape, dtype=np.uint8) * np.random.rand(*shape) * 255 x = x.astype(np.uint8) f = NamedTemporaryFile(suffix='.tif') fname = f.name @@ -93,7 +93,7 @@ class TestSave: ]: tests = [(d, f) for d in dtype for f in format] for d, f in tests: - x = np.ones(shape, dtype=d) * np.random.random(shape) + x = np.ones(shape, dtype=d) * np.random.rand(*shape) if not np.issubdtype(d, float): x = (x * 255).astype(d) yield self.roundtrip, d, x, f diff --git a/skimage/io/tests/test_imread.py b/skimage/io/tests/test_imread.py index b4886970..25ad870e 100644 --- a/skimage/io/tests/test_imread.py +++ b/skimage/io/tests/test_imread.py @@ -68,7 +68,7 @@ class TestSave: def test_imsave_roundtrip(self): dtype = np.uint8 for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: - x = np.ones(shape, dtype=dtype) * np.random.random(shape) + x = np.ones(shape, dtype=dtype) * np.random.rand(*shape) if np.issubdtype(dtype, float): yield self.roundtrip, x, 255 diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index 1b534aa5..c5718388 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -126,7 +126,7 @@ class TestSave: def test_imsave_roundtrip(self): for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: for dtype in (np.uint8, np.uint16, np.float32, np.float64): - x = np.ones(shape, dtype=dtype) * np.random.random(shape) + x = np.ones(shape, dtype=dtype) * np.random.rand(*shape) if np.issubdtype(dtype, float): yield self.roundtrip, dtype, x, 255 diff --git a/skimage/io/tests/test_plugin_util.py b/skimage/io/tests/test_plugin_util.py index 7b337a94..30f67018 100644 --- a/skimage/io/tests/test_plugin_util.py +++ b/skimage/io/tests/test_plugin_util.py @@ -8,10 +8,10 @@ np.random.seed(0) class TestPrepareForDisplay: def test_basic(self): - prepare_for_display(np.random.random((10, 10))) + prepare_for_display(np.random.rand(10, 10)) def test_dtype(self): - x = prepare_for_display(np.random.random((10, 15))) + x = prepare_for_display(np.random.rand(10, 15)) assert x.dtype == np.dtype(np.uint8) def test_grey(self): @@ -21,18 +21,18 @@ class TestPrepareForDisplay: assert x[3, 2, 0] == 255 def test_colour(self): - prepare_for_display(np.random.random((10, 10, 3))) + prepare_for_display(np.random.rand(10, 10, 3)) def test_alpha(self): - prepare_for_display(np.random.random((10, 10, 4))) + prepare_for_display(np.random.rand(10, 10, 4)) @raises(ValueError) def test_wrong_dimensionality(self): - prepare_for_display(np.random.random((10, 10, 1, 1))) + prepare_for_display(np.random.rand(10, 10, 1, 1)) @raises(ValueError) def test_wrong_depth(self): - prepare_for_display(np.random.random((10, 10, 5))) + prepare_for_display(np.random.rand(10, 10, 5)) class TestWindowManager: diff --git a/skimage/io/tests/test_simpleitk.py b/skimage/io/tests/test_simpleitk.py index 51f94986..ab8cd592 100644 --- a/skimage/io/tests/test_simpleitk.py +++ b/skimage/io/tests/test_simpleitk.py @@ -88,7 +88,7 @@ class TestSave: def test_imsave_roundtrip(self): for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: for dtype in (np.uint8, np.uint16, np.float32, np.float64): - x = np.ones(shape, dtype=dtype) * np.random.random(shape) + x = np.ones(shape, dtype=dtype) * np.random.rand(*shape) if np.issubdtype(dtype, float): yield self.roundtrip, dtype, x diff --git a/skimage/io/tests/test_tifffile.py b/skimage/io/tests/test_tifffile.py index 00c7f41c..7019f0c6 100644 --- a/skimage/io/tests/test_tifffile.py +++ b/skimage/io/tests/test_tifffile.py @@ -51,7 +51,7 @@ class TestSave: def test_imsave_roundtrip(self): for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: for dtype in (np.uint8, np.uint16, np.float32, np.float64): - x = np.ones(shape, dtype=dtype) * np.random.random(shape) + x = np.ones(shape, dtype=dtype) * np.random.rand(*shape) if not np.issubdtype(dtype, float): x = (x * 255).astype(dtype) diff --git a/skimage/measure/fit.py b/skimage/measure/fit.py index 8f9ceb9c..e0118388 100644 --- a/skimage/measure/fit.py +++ b/skimage/measure/fit.py @@ -586,7 +586,8 @@ def ransac(data, model_class, min_samples, residual_threshold, Robustly estimate geometric transformation: >>> from skimage.transform import SimilarityTransform - >>> src = 100 * np.random.random((50, 2)) + >>> np.random.seed(0) + >>> src = 100 * np.random.rand(50, 2) >>> model0 = SimilarityTransform(scale=0.5, rotation=1, ... translation=(10, 20)) >>> dst = model0(src) diff --git a/skimage/measure/tests/test_structural_similarity.py b/skimage/measure/tests/test_structural_similarity.py index e08f2c31..35bbeeb8 100644 --- a/skimage/measure/tests/test_structural_similarity.py +++ b/skimage/measure/tests/test_structural_similarity.py @@ -9,8 +9,8 @@ np.random.seed(1234) def test_ssim_patch_range(): N = 51 - X = (np.random.random((N, N)) * 255).astype(np.uint8) - Y = (np.random.random((N, N)) * 255).astype(np.uint8) + X = (np.random.rand(N, N) * 255).astype(np.uint8) + Y = (np.random.rand(N, N) * 255).astype(np.uint8) assert(ssim(X, Y, win_size=N) < 0.1) assert_equal(ssim(X, X, win_size=N), 1) @@ -18,8 +18,8 @@ def test_ssim_patch_range(): def test_ssim_image(): N = 100 - X = (np.random.random((N, N)) * 255).astype(np.uint8) - Y = (np.random.random((N, N)) * 255).astype(np.uint8) + X = (np.random.rand(N, N) * 255).astype(np.uint8) + Y = (np.random.rand(N, N) * 255).astype(np.uint8) S0 = ssim(X, X, win_size=3) assert_equal(S0, 1) @@ -31,8 +31,8 @@ def test_ssim_image(): # NOTE: This test is known to randomly fail on some systems (Mac OS X 10.6) def test_ssim_grad(): N = 30 - X = np.random.random((N, N)) * 255 - Y = np.random.random((N, N)) * 255 + X = np.random.rand(N, N) * 255 + Y = np.random.rand(N, N) * 255 f = ssim(X, Y, dynamic_range=255) g = ssim(X, Y, dynamic_range=255, gradient=True) @@ -44,8 +44,8 @@ def test_ssim_grad(): def test_ssim_dtype(): N = 30 - X = np.random.random((N, N)) - Y = np.random.random((N, N)) + X = np.random.rand(N, N) + Y = np.random.rand(N, N) S1 = ssim(X, Y) diff --git a/skimage/morphology/tests/test_ccomp.py b/skimage/morphology/tests/test_ccomp.py index 48c8a850..fb596481 100644 --- a/skimage/morphology/tests/test_ccomp.py +++ b/skimage/morphology/tests/test_ccomp.py @@ -27,7 +27,7 @@ class TestConnectedComponents: assert self.x[0, 2] == 3 def test_random(self): - x = (np.random.random((20, 30)) * 5).astype(np.int) + x = (np.random.rand(20, 30) * 5).astype(np.int) with catch_warnings(): labels = label(x) diff --git a/skimage/restoration/tests/test_denoise.py b/skimage/restoration/tests/test_denoise.py index d451aa74..c1091009 100644 --- a/skimage/restoration/tests/test_denoise.py +++ b/skimage/restoration/tests/test_denoise.py @@ -15,7 +15,7 @@ def test_denoise_tv_chambolle_2d(): # lena image img = lena_gray # add noise to lena - img += 0.5 * img.std() * np.random.random(img.shape) + img += 0.5 * img.std() * np.random.rand(*img.shape) # clip noise so that it does not exceed allowed range for float images. img = np.clip(img, 0, 1) # denoise @@ -57,7 +57,7 @@ def test_denoise_tv_chambolle_3d(): mask = (x - 22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 mask = 100 * mask.astype(np.float) mask += 60 - mask += 20 * np.random.random(mask.shape) + mask += 20 * np.random.rand(*mask.shape) mask[mask < 0] = 0 mask[mask > 255] = 255 res = restoration.denoise_tv_chambolle(mask.astype(np.uint8), weight=100) @@ -66,13 +66,13 @@ def test_denoise_tv_chambolle_3d(): # test wrong number of dimensions assert_raises(ValueError, restoration.denoise_tv_chambolle, - np.random.random((8, 8, 8, 8))) + np.random.rand(8, 8, 8, 8)) def test_denoise_tv_bregman_2d(): img = lena_gray # add some random noise - img += 0.5 * img.std() * np.random.random(img.shape) + img += 0.5 * img.std() * np.random.rand(*img.shape) img = np.clip(img, 0, 1) out1 = restoration.denoise_tv_bregman(img, weight=10) @@ -98,7 +98,7 @@ def test_denoise_tv_bregman_float_result_range(): def test_denoise_tv_bregman_3d(): img = lena # add some random noise - img += 0.5 * img.std() * np.random.random(img.shape) + img += 0.5 * img.std() * np.random.rand(*img.shape) img = np.clip(img, 0, 1) out1 = restoration.denoise_tv_bregman(img, weight=10) @@ -112,7 +112,7 @@ def test_denoise_tv_bregman_3d(): def test_denoise_bilateral_2d(): img = lena_gray # add some random noise - img += 0.5 * img.std() * np.random.random(img.shape) + img += 0.5 * img.std() * np.random.rand(*img.shape) img = np.clip(img, 0, 1) out1 = restoration.denoise_bilateral(img, sigma_range=0.1, @@ -128,7 +128,7 @@ def test_denoise_bilateral_2d(): def test_denoise_bilateral_3d(): img = lena # add some random noise - img += 0.5 * img.std() * np.random.random(img.shape) + img += 0.5 * img.std() * np.random.rand(*img.shape) img = np.clip(img, 0, 1) out1 = restoration.denoise_bilateral(img, sigma_range=0.1, diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 731518e3..f5b265a2 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -316,7 +316,8 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, Examples -------- - >>> a = np.zeros((10, 10)) + 0.2 * np.random.random((10, 10)) + >>> np.random.seed(0) + >>> a = np.zeros((10, 10)) + 0.2 * np.random.rand(10, 10) >>> a[5:8, 5:8] += 1 >>> b = np.zeros_like(a) >>> b[3, 3] = 1 # Marker for first phase @@ -331,7 +332,7 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, [1, 1, 1, 1, 1, 2, 2, 2, 1, 1], [1, 1, 1, 1, 1, 2, 2, 2, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], dtype=int32) + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]) """ # Parse input data diff --git a/skimage/transform/tests/test_integral.py b/skimage/transform/tests/test_integral.py index c9182a0e..eb92a40b 100644 --- a/skimage/transform/tests/test_integral.py +++ b/skimage/transform/tests/test_integral.py @@ -4,14 +4,14 @@ from numpy.testing import assert_equal from skimage.transform import integral_image, integrate np.random.seed(0) -x = (np.random.random((50, 50)) * 255).astype(np.uint8) +x = (np.random.rand(50, 50) * 255).astype(np.uint8) s = integral_image(x) def test_validity(): y = np.arange(12).reshape((4, 3)) - y = (np.random.random((50, 50)) * 255).astype(np.uint8) + y = (np.random.rand(50, 50) * 255).astype(np.uint8) assert_equal(integral_image(y)[-1, -1], y.sum()) diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 66c867df..5c210d9b 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -252,7 +252,7 @@ def test_inverse(): def test_slow_warp_nonint_oshape(): - image = np.random.random((5, 5)) + image = np.random.rand(5, 5) assert_raises(ValueError, warp, image, lambda xy: xy, output_shape=(13.1, 19.5)) From caaf75ebd1e4120458a175ce6d580678ba8ec9d4 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sat, 19 Jul 2014 19:16:46 -0500 Subject: [PATCH 0168/1122] Fix failing doctests --- skimage/feature/brief.py | 4 ++-- skimage/segmentation/random_walker_segmentation.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/feature/brief.py b/skimage/feature/brief.py index cc926113..d1626f17 100644 --- a/skimage/feature/brief.py +++ b/skimage/feature/brief.py @@ -71,7 +71,7 @@ class BRIEF(DescriptorExtractor): [0, 0, 1, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0]], dtype=np.int32) + [0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32) >>> square2 = np.zeros((9, 9), dtype=np.int32) >>> square2[2:7, 2:7] = 1 >>> square2 @@ -83,7 +83,7 @@ class BRIEF(DescriptorExtractor): [0, 0, 1, 1, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=np.int32) + [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32) >>> keypoints1 = corner_peaks(corner_harris(square1), min_distance=1) >>> keypoints2 = corner_peaks(corner_harris(square2), min_distance=1) >>> extractor = BRIEF(patch_size=5) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index f5b265a2..bcb57739 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -332,7 +332,7 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, [1, 1, 1, 1, 1, 2, 2, 2, 1, 1], [1, 1, 1, 1, 1, 2, 2, 2, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]) + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], dtype=int32) """ # Parse input data From 7d14f5cbcf7177a4145f1e289b48dcfa6016c650 Mon Sep 17 00:00:00 2001 From: Fedor Baart Date: Thu, 24 Jul 2014 13:10:50 +0200 Subject: [PATCH 0169/1122] setup --- setup.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index e8df6dad..640329fc 100755 --- a/setup.py +++ b/setup.py @@ -19,9 +19,11 @@ LICENSE = 'Modified BSD' DOWNLOAD_URL = 'http://github.com/scikit-image/scikit-image' VERSION = '0.11dev' PYTHON_VERSION = (2, 5) + +# These are manually checked. +# These packages are sometimes installed outside of the setuptools scope DEPENDENCIES = { 'numpy': (1, 6), - 'six': (1, 3), } # Only require Cython if we have a developer checkout @@ -139,7 +141,9 @@ if __name__ == "__main__": ], configuration=configuration, - + install_requires=[ + "six>=1.3" + ], packages=setuptools.find_packages(exclude=['doc']), include_package_data=True, zip_safe=False, # the package can run out of an .egg file From 278a0d6862e25a479f38a29f5451f33232fdc3cf Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 28 Jul 2014 12:38:34 +1000 Subject: [PATCH 0170/1122] Bug fix: nbins can be >256 in threshold_isodata The threshold_isodata function created an arange of values up to the number of bins, but gave it type np.uint8, limiting the number of bins to 256. Fixes #1085. --- skimage/filter/tests/test_thresholding.py | 6 ++++++ skimage/filter/thresholding.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/skimage/filter/tests/test_thresholding.py b/skimage/filter/tests/test_thresholding.py index f0a68f7d..47555a83 100644 --- a/skimage/filter/tests/test_thresholding.py +++ b/skimage/filter/tests/test_thresholding.py @@ -67,6 +67,12 @@ class TestSimpleImage(): def test_isodata_linspace(self): assert -63.8 < threshold_isodata(np.linspace(-127, 0, 256)) < -63.6 + def test_isodata_16bit(): + np.random.seed(0) + imfloat = np.random.rand(256, 256) + t = threshold_isodata(imfloat, nbins=1024) + assert 0.49 < t < 0.51 + def test_threshold_adaptive_generic(self): def func(arr): return arr.sum() / arr.shape[0] diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index 6ccf8f6b..da74fe47 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -243,7 +243,7 @@ def threshold_isodata(image, nbins=256): cpmfl = np.cumsum(pmf, dtype=np.float32) cpmfh = np.cumsum(pmf[::-1], dtype=np.float32)[::-1] - binnums = np.arange(pmf.size, dtype=np.uint8) + binnums = np.arange(pmf.size, dtype=np.min_scalar_type(nbins)) # l and h contain average value of pixels in sum of bins, calculated # from lower to higher and from higher to lower respectively. l = np.ma.divide(np.cumsum(pmf * binnums, dtype=np.float32), cpmfl) From 838617cb394c9ab9c948ebd64439d0485edfbf0d Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 28 Jul 2014 13:01:41 +1000 Subject: [PATCH 0171/1122] Fix incorrect test function signature --- skimage/filter/tests/test_thresholding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/filter/tests/test_thresholding.py b/skimage/filter/tests/test_thresholding.py index 47555a83..8464cb21 100644 --- a/skimage/filter/tests/test_thresholding.py +++ b/skimage/filter/tests/test_thresholding.py @@ -67,7 +67,7 @@ class TestSimpleImage(): def test_isodata_linspace(self): assert -63.8 < threshold_isodata(np.linspace(-127, 0, 256)) < -63.6 - def test_isodata_16bit(): + def test_isodata_16bit(self): np.random.seed(0) imfloat = np.random.rand(256, 256) t = threshold_isodata(imfloat, nbins=1024) From b6dcf3c336281464b4b344b77ab1fae1164a15ab Mon Sep 17 00:00:00 2001 From: blink1073 Date: Fri, 18 Jul 2014 16:12:51 -0500 Subject: [PATCH 0172/1122] Update Travis build to use Anaconda. Update Travis build to use Anaconda Travis updates and fixes More travis fixes Another travis attempt Revert changes Use PIL and Pillow Refactor travis into 4 different builds Fix activation error Remove explicit mpl in build_versions.py Make matplotlib an explicit requirement Rearrange travis Make pillow a hard requirement Try again to make Pillow optional Fix bash syntax error Fix bash syntax error Bump required cython version More rearrangments Remove mpl from build_versions, rearrange travis Fix version check Make matplotlib explicit again Conda install into test env Check for proper install Allow tests to skip if networkx is not available Allow tests to skip if networkx is not available Try swapping pillow for matplotlib Allow tests to pass when matplotlib is not present Remove matplotlib from build_versions Print PIL version Get pillow from PIP Allow tests to skip if matplotlib is not present. Allow tests to skip if networkx is not present. travis fix Remove unused mpl import that caused test error Use nose-cov and do not run doctests without optional libs Bump required numpy version and fix nose calls Make overlay test repeatable bump numpy version again Move low-end numpy to python 2.7 Play with minimum versions Add version requirements and use functions Add version requirements and use functions Allow require to skip a test More implementation of require decorator Update require decorator and clean up tests Only use requires decorator when needed Fix python3 error in version_requirements Fix build errors Fix handling of require with tests More fixes for require handler Use latest miniconda Fix more build errors Fix another dict comprehension and travis file. Fix missing imports Fix dictionary again Fix import warning Fix last failing test on 2.6 Skip doc examples on python2.6 Do not run doctests on python2.6 Fix typo in travis.yml Make numpy-1.6 compatibility changes Use numpy-1.6 in travis python2.6 Add tests for version requirements Fix line noise in PR Add additional io plugins Fix simpleitk test. Fix python 3 error in freeimage_plugin. Install imread in Travis. Put matplotlib settings in XDG recommended directory Fix formatting in travis yml Fix formatting in travis yml Make sure to close PIL file atexit Fix name of apt package xcftools Fix pil fp closing Fix matplotlibrc creation Only download SimpleITK on py2x, run coverage on py27 Fix travis yml syntax error Run coveralls on py2.7 Install SimpleITK on py3.3 and run coverage on py3.3 Make simpleitk install quiet Use standard nose and clean up incantation Fix travis yml syntax error Put in miniconda workout for libc error. Fix imread plugin. Fix travis syntax Remove unused import Remove miniconda libpng in favor of system png Fix imread install and move libm removal to after optional pkg install. Fix png header copy in travis yml Another attempt to use png headers Debug freeimage Add jpeg library for freeimage and debug imread. More debug for imread and freeimage More freeimage and imread debugging More debugging Use correct paths for test env Make sure imread is tied to libpng15 Add a TODO note for simpleitk test causing error. Fix typo in yml Cleanup and add more comments to travis yml Update comment Try and add 3.2 support. Docstring formatting Add more travis comments. Try numpy 1.6 on python 2.7 Fix travis syntax error Rename CONDA to ENV for clarity Alias python on python 3.2 Use python 3.2 as the system python Clean up libfreeimage install Fix order on py3.2 pre_install Move old numpy back to py26 Use the appropriate python calls. Debug 3.2 build. Update comment Fix syntax error Another fix for syntax error. Install scipy after downloading import tools More debugging for py32 Do not install conda on py3.2 (duh) Fix typo in travis yml Fix py32 qt install, separate pyfits and imread to find error Fix syntax error and front-load option lib check for debug pyfits is not supported in py3.2, try imread now imread is also not supported on py3.2 install imread before pyfits to show relationship with libs Make pip builds quiet Minor formatting to retrigger build Allow simpleitk to fail to download without breaking the build Use travis_retry for SimpleITK See what breaks when we keep libm in Now remove libm again --- .travis.yml | 145 ++++++++++-------- skimage/exposure/_adapthist.py | 4 +- skimage/feature/tests/test_util.py | 6 +- skimage/graph/graph_cut.py | 7 +- skimage/graph/rag.py | 9 +- skimage/graph/tests/test_rag.py | 3 + skimage/io/_plugins/freeimage_plugin.py | 2 +- skimage/io/_plugins/imread_plugin.py | 2 +- skimage/io/_plugins/pil_plugin.py | 6 +- skimage/io/_plugins/qt_plugin.py | 2 +- skimage/io/tests/test_imread.py | 5 +- skimage/io/tests/test_simpleitk.py | 10 +- skimage/morphology/greyreconstruct.py | 2 +- skimage/morphology/misc.py | 5 +- skimage/restoration/tests/test_unwrap.py | 7 + skimage/restoration/unwrap.py | 2 + .../util/tests/test_version_requirements.py | 46 ++++++ skimage/util/version_requirements.py | 125 +++++++++++++++ skimage/viewer/__init__.py | 10 ++ skimage/viewer/canvastools/base.py | 4 +- skimage/viewer/canvastools/linetool.py | 3 +- skimage/viewer/canvastools/painttool.py | 12 +- skimage/viewer/canvastools/recttool.py | 1 - skimage/viewer/plugins/color_histogram.py | 6 +- skimage/viewer/plugins/overlayplugin.py | 11 +- skimage/viewer/tests/test_plugins.py | 20 +-- skimage/viewer/tests/test_tools.py | 18 +-- skimage/viewer/tests/test_utils.py | 21 ++- skimage/viewer/tests/test_viewer.py | 20 +-- skimage/viewer/tests/test_widgets.py | 15 +- skimage/viewer/utils/core.py | 11 +- skimage/viewer/viewers/core.py | 1 - tools/build_versions.py | 11 +- 33 files changed, 380 insertions(+), 172 deletions(-) create mode 100644 skimage/util/tests/test_version_requirements.py create mode 100644 skimage/util/version_requirements.py diff --git a/.travis.yml b/.travis.yml index 1f292dfa..1ef5b85d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,28 +3,17 @@ # After changing this file, check it on: # http://lint.travis-ci.org/ - language: python python: - - 2.6 + - 3.2 -matrix: - include: - - python: 2.7 - env: - - PYTHON=python - - PYTHONWARNINGS=all - - PYTHONX=python - - PYVER=2.x - - python: 3.2 - env: - - PYTHON=python3 - - PYTHONWARNINGS=all - - PYTHONX=python3 - - PYVER=3.x - exclude: - - python: 2.6 +env: + - ENV="python=2.6 numpy=1.6 cython=0.19.2" + - ENV="python=2.7 numpy cython" + - ENV="python=3.2" + - ENV="python=3.3 numpy cython" + - ENV="python=3.4 numpy cython" virtualenv: system_site_packages: true @@ -32,64 +21,92 @@ virtualenv: before_install: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start - - sudo apt-get update - - sudo apt-get install $PYTHON-numpy - - wget https://raw.githubusercontent.com/numpy/numpy/master/numpy/_import_tools.py -O /home/travis/virtualenv/python3.2_with_system_site_packages/lib/python3.2/site-packages/numpy/_import_tools.py - - - sudo apt-get install $PYTHON-scipy - - sudo apt-get install libfreeimage3 - - - if [[ $PYVER == '2.x' ]]; then - - sudo apt-get install $PYTHON-qt4; - - sudo apt-get install $PYTHON-matplotlib; - - fi - - if [[ $PYVER == '3.x' ]]; then - - sudo apt-get install $PYTHON-pyqt4; - - pip install --use-mirrors matplotlib; - - fi - - - pip install pillow - - pip install cython - - pip install flake8 - - pip install six - - pip install networkx - - - pip install nose-cov - - pip install coveralls - + - if [[ $ENV == python=3.2 ]]; then + sudo apt-get install python3-numpy; + wget https://raw.githubusercontent.com/numpy/numpy/master/numpy/_import_tools.py -O /home/travis/virtualenv/python3.2_with_system_site_packages/lib/python3.2/site-packages/numpy/_import_tools.py; + sudo apt-get install python3-scipy; + pip install cython flake8 six; + else + wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; + bash miniconda.sh -b -p $HOME/miniconda; + export PATH="$HOME/miniconda/bin:$PATH"; + hash -r; + conda config --set always_yes yes; + conda update conda; + conda info -a; + conda create -n test $ENV six scipy pip flake8 nose; + source activate test; + fi + - pip install coveralls pillow - python check_bento_build.py install: - tools/header.py "Dependency versions" - tools/build_versions.py - + - export PYTHONWARNINGS=all - python setup.py build_ext --inplace script: - # Matplotlib settings - - mkdir -p $HOME/.matplotlib - - touch $HOME/.matplotlib/matplotlibrc - - "echo 'backend : Agg' > $HOME/.matplotlib/matplotlibrc" - - "echo 'backend.qt4 : PyQt4' >> $HOME/.matplotlib/matplotlibrc" - - # Run all tests - - if [[ $PYVER == '3.x' ]]; then - - nosetests --exe -v --with-doctest --with-cov --cov skimage --cov-config=.coveragerc skimage - - fi - - if [[ $PYVER == '2.x' ]]; then - - nosetests --exe -v --with-doctest skimage - - fi - # Run all doc examples - - export PYTHONPATH=$(pwd):$PYTHONPATH - - for f in doc/examples/*.py; do $PYTHONX "$f"; if [ $? -ne 0 ]; then exit 1; fi done - - for f in doc/examples/applications/*.py; do $PYTHONX "$f"; if [ $? -ne 0 ]; then exit 1; fi done + # Run all tests with minimum dependencies + - nosetests --exe -v skimage # Run pep8 and flake tests - flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples + # Install optional dependencies to get full test coverage + # Notes: + # - pyfits and imread do NOT support py3.2 + # - Use the png headers included in anaconda (from matplotlib install) + # TODO: Remove the libm removal when anaconda fixes their libraries + # The solution was suggested here + # https://groups.google.com/a/continuum.io/forum/#!topic/anaconda/-DLG2ZdTkw0 + - if [[ $ENV == python=3.2 ]]; then + sudo apt-get install -qq python3-pyqt4; + pip install --use-mirrors -q matplotlib; + pip install -q networkx; + else + conda install -q matplotlib pyqt networkx; + sudo cp ~/miniconda/envs/test/include/png* /usr/include; + rm ~/miniconda/envs/test/lib/libm-2.5.so; + rm ~/miniconda/envs/test/lib/libm.*; + sudo apt-get install -qq libtiff4-dev libwebp-dev xcftools; + pip install -q imread; + pip install -q pyfits; + fi + - sudo apt-get install -qq libfreeimage3 + # TODO: update when SimpleITK become available on py34 or hopefully pip + - if [[ $ENV != python=3.4* ]]; then + travis_retry easy_install -q SimpleITK; + fi + + # Matplotlib settings + - export MPL_DIR=$HOME/.config/matplotlib + - mkdir -p $MPL_DIR + - touch $MPL_DIR/matplotlibrc + - "echo 'backend : Agg' > $MPL_DIR/matplotlibrc" + - "echo 'backend.qt4 : PyQt4' >> $MPL_DIR/matplotlibrc" + + # Run all doc examples + - export PYTHONPATH=$(pwd):$PYTHONPATH + - if [[ $ENV != python=2.6* ]]; then + for f in doc/examples/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done + fi + - for f in doc/examples/applications/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done + + # run tests again with optional dependencies to get more coverage + # measure coverage on py3.3 + # doctests fail on python 2.6 for restoration/unwrap + - if [[ $ENV == python=3.3* ]]; then + nosetests --exe -v --with-doctest --with-cov --cover-package skimage; + elif [[ $ENV == python=2.6* ]]; then + nosetests --exe -v skimage; + else + nosetests --exe -v --with-doctest skimage; + fi + after_success: - - if [[ $PYVER == '3.x' ]]; then - - coveralls - - fi + - if [[ $ENV == python=3.3* ]]; then + coveralls; + fi diff --git a/skimage/exposure/_adapthist.py b/skimage/exposure/_adapthist.py index 5da50029..9cfc716e 100644 --- a/skimage/exposure/_adapthist.py +++ b/skimage/exposure/_adapthist.py @@ -17,7 +17,7 @@ import numpy as np import skimage from skimage.color.adapt_rgb import adapt_rgb, hsv_value from skimage.exposure import rescale_intensity -from skimage.util import view_as_blocks +from skimage.util import view_as_blocks, pad MAX_REG_X = 16 # max. # contextual regions in x-direction */ @@ -125,7 +125,7 @@ def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): if h_inner != image.shape[1] or w_inner != image.shape[0]: h_pad = height * ntiles_y - image.shape[0] w_pad = width * ntiles_x - image.shape[1] - image = np.pad(image, ((0, h_pad), (0, w_pad)), 'reflect') + image = pad(image, ((0, h_pad), (0, w_pad)), mode='reflect') h_inner, w_inner = image.shape bin_size = 1 + NR_OF_GREY / nbins diff --git a/skimage/feature/tests/test_util.py b/skimage/feature/tests/test_util.py index 2b601b94..f15bcb2a 100644 --- a/skimage/feature/tests/test_util.py +++ b/skimage/feature/tests/test_util.py @@ -1,5 +1,8 @@ import numpy as np -import matplotlib.pyplot as plt +try: + import matplotlib.pyplot as plt +except ImportError: + plt = None from numpy.testing import assert_equal, assert_raises from skimage.feature.util import (FeatureDetector, DescriptorExtractor, @@ -39,6 +42,7 @@ def test_mask_border_keypoints(): [0, 0, 0, 0, 1]) +@np.testing.decorators.skipif(plt is None) def test_plot_matches(): fig, ax = plt.subplots(nrows=1, ncols=1) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 91d64aec..443b1561 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -1,4 +1,9 @@ -import networkx as nx +try: + import networkx as nx +except ImportError: + import warnings + warnings.warn('"cut_threshold" requires networkx') + nx = None import numpy as np diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 84721736..fc4a1aaa 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -1,4 +1,11 @@ -import networkx as nx +try: + import networkx as nx +except ImportError: + class nx: + class Graph: + pass + import warnings + warnings.warn('Region Adjacency Graph (RAG) features require networkx') import numpy as np from scipy.ndimage import filters from scipy import ndimage as nd diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index af7481eb..b6f95cb2 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -1,5 +1,6 @@ import numpy as np from skimage import graph +from skimage.util.version_requirements import require def max_edge(g, src, dst, n): @@ -9,6 +10,7 @@ def max_edge(g, src, dst, n): return max(w1, w2) +@require('networkx') def test_rag_merge(): g = graph.rag.RAG() @@ -43,6 +45,7 @@ def test_rag_merge(): assert g.edges() == [] +@require('networkx') def test_threshold_cut(): img = np.zeros((100, 100, 3), dtype='uint8') diff --git a/skimage/io/_plugins/freeimage_plugin.py b/skimage/io/_plugins/freeimage_plugin.py index 07e8d313..5ebe8214 100644 --- a/skimage/io/_plugins/freeimage_plugin.py +++ b/skimage/io/_plugins/freeimage_plugin.py @@ -85,7 +85,7 @@ def load_freeimage(): if errors: # No freeimage library loaded, and load-errors reported for some # candidate libs - err_txt = ['%s:\n%s' % (l, str(e.message)) for l, e in errors] + err_txt = ['%s:\n%s' % (l, str(e)) for l, e in errors] raise RuntimeError('One or more FreeImage libraries were found, but ' 'could not be loaded due to the following errors:\n' '\n\n'.join(err_txt)) diff --git a/skimage/io/_plugins/imread_plugin.py b/skimage/io/_plugins/imread_plugin.py index 46382cdf..cb715e57 100644 --- a/skimage/io/_plugins/imread_plugin.py +++ b/skimage/io/_plugins/imread_plugin.py @@ -1,6 +1,6 @@ __all__ = ['imread', 'imsave'] -from skimage.utils.dtype import convert +from skimage.util.dtype import convert try: import imread as _imread diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 88e7f04b..e6f9c491 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -21,6 +21,7 @@ def imread(fname, dtype=None): """ im = Image.open(fname) + fp = im.fp if im.mode == 'P': if _palette_is_grayscale(im): im = im.convert('L') @@ -35,8 +36,9 @@ def imread(fname, dtype=None): im.shape = shape[::-1] elif 'A' in im.mode: im = im.convert('RGBA') - - return np.array(im, dtype=dtype) + im = np.array(im, dtype=dtype) + fp.close() + return im def _palette_is_grayscale(pil_image): diff --git a/skimage/io/_plugins/qt_plugin.py b/skimage/io/_plugins/qt_plugin.py index db4cb4cc..f3a3b8e0 100644 --- a/skimage/io/_plugins/qt_plugin.py +++ b/skimage/io/_plugins/qt_plugin.py @@ -158,7 +158,7 @@ def imsave(filename, img, format_str=None): qbuffer.open(QtCore.QIODevice.ReadWrite) saved = qimg.save(qbuffer, format_str.upper()) qbuffer.seek(0) - filename.write(qbuffer.readAll()) + filename.write(qbuffer.readAll().data()) qbuffer.close() else: saved = qimg.save(filename) diff --git a/skimage/io/tests/test_imread.py b/skimage/io/tests/test_imread.py index 25ad870e..a2c32b79 100644 --- a/skimage/io/tests/test_imread.py +++ b/skimage/io/tests/test_imread.py @@ -7,6 +7,7 @@ from tempfile import NamedTemporaryFile from skimage import data_dir from skimage.io import imread, imsave, use_plugin, reset_plugins +import skimage.io as sio try: import imread as _imread @@ -39,11 +40,13 @@ def test_imread_palette(): img = imread(os.path.join(data_dir, 'palette_color.png')) assert img.ndim == 3 + @skipif(not imread_available) def test_imread_truncated_jpg(): assert_raises((RuntimeError, ValueError), sio.imread, - os.path.join(si.data_dir, 'truncated.jpg')) + os.path.join(data_dir, 'truncated.jpg')) + @skipif(not imread_available) def test_bilevel(): diff --git a/skimage/io/tests/test_simpleitk.py b/skimage/io/tests/test_simpleitk.py index ab8cd592..e7fb1e83 100644 --- a/skimage/io/tests/test_simpleitk.py +++ b/skimage/io/tests/test_simpleitk.py @@ -1,6 +1,7 @@ import os.path import numpy as np from numpy.testing.decorators import skipif +from numpy.testing import assert_raises from tempfile import NamedTemporaryFile @@ -52,12 +53,15 @@ def test_bilevel(): img = imread(os.path.join(data_dir, 'checker_bilevel.png')) np.testing.assert_array_equal(img, expected) - +""" +#TODO: This test causes a Segmentation fault @skipif(not sitk_available) def test_imread_truncated_jpg(): assert_raises((RuntimeError, ValueError), - sio.imread, - os.path.join(si.data_dir, 'truncated.jpg')) + imread, + os.path.join(data_dir, 'truncated.jpg')) +""" + @skipif(not sitk_available) def test_imread_uint16(): diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index cea200a4..bce26d6b 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -131,7 +131,7 @@ def reconstruction(seed, mask, method='dilation', selem=None, offset=None): if selem is None: selem = np.ones([3] * seed.ndim, dtype=bool) else: - selem = selem.astype(bool, copy=True) + selem = selem.astype(bool) if offset is None: if not all([d % 2 == 1 for d in selem.shape]): diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index 94aadf7a..edf2892b 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -5,12 +5,13 @@ from .selem import _default_selem # Our function names don't exactly correspond to ndimages. # This dictionary translates from our names to scipy's. funcs = ('erosion', 'dilation', 'opening', 'closing') -skimage2ndimage = {x: 'grey_' + x for x in funcs} +skimage2ndimage = dict((x, 'grey_' + x) for x in funcs) # These function names are the same in ndimage. funcs = ('binary_erosion', 'binary_dilation', 'binary_opening', 'binary_closing', 'black_tophat', 'white_tophat') -skimage2ndimage.update({x: x for x in funcs}) +skimage2ndimage.update(dict((x, x) for x in funcs)) + def default_fallback(func): """Decorator to fall back on ndimage for images with more than 2 dimensions diff --git a/skimage/restoration/tests/test_unwrap.py b/skimage/restoration/tests/test_unwrap.py index fca63724..2c2381e3 100644 --- a/skimage/restoration/tests/test_unwrap.py +++ b/skimage/restoration/tests/test_unwrap.py @@ -7,6 +7,7 @@ from numpy.testing import (run_module_suite, assert_array_almost_equal, import warnings from skimage.restoration import unwrap_phase +from skimage.util.version_requirements import require def assert_phase_almost_equal(a, b, *args, **kwargs): @@ -41,6 +42,7 @@ def check_unwrap(image, mask=None): assert_phase_almost_equal(image_unwrapped, image) +@require('python', '>=2.7') def test_unwrap_1d(): image = np.linspace(0, 10 * np.pi, 100) check_unwrap(image) @@ -50,6 +52,7 @@ def test_unwrap_1d(): assert_raises(ValueError, unwrap_phase, image, True) +@require('python', '>=2.7') def test_unwrap_2d(): x, y = np.ogrid[:8, :16] image = 2 * np.pi * (x * 0.2 + y * 0.1) @@ -59,6 +62,7 @@ def test_unwrap_2d(): yield check_unwrap, image, mask +@require('python', '>=2.7') def test_unwrap_3d(): x, y, z = np.ogrid[:8, :12, :16] image = 2 * np.pi * (x * 0.2 + y * 0.1 + z * 0.05) @@ -104,12 +108,14 @@ def check_wrap_around(ndim, axis): image_unwrap_wrap_around[index_last]) +@require('python', '>=2.7') def test_wrap_around(): for ndim in (2, 3): for axis in range(ndim): yield check_wrap_around, ndim, axis +@require('python', '>=2.7') def test_mask(): length = 100 ramps = [np.linspace(0, 4 * np.pi, length), @@ -137,6 +143,7 @@ def test_mask(): assert_array_almost_equal(image_unwrapped_3d[:, :, -1], image[i, -1]) +@require('python', '>=2.7') def test_invalid_input(): assert_raises(ValueError, unwrap_phase, np.zeros([])) assert_raises(ValueError, unwrap_phase, np.zeros((1, 1, 1, 1))) diff --git a/skimage/restoration/unwrap.py b/skimage/restoration/unwrap.py index 085bef0b..14da2677 100644 --- a/skimage/restoration/unwrap.py +++ b/skimage/restoration/unwrap.py @@ -1,12 +1,14 @@ import numpy as np import warnings from six import string_types +from skimage.util.version_requirements import require from ._unwrap_1d import unwrap_1d from ._unwrap_2d import unwrap_2d from ._unwrap_3d import unwrap_3d +@require('python', '>=2.7') def unwrap_phase(image, wrap_around=False): '''From ``image``, wrapped to lie in the interval [-pi, pi), recover the original, unwrapped image. diff --git a/skimage/util/tests/test_version_requirements.py b/skimage/util/tests/test_version_requirements.py new file mode 100644 index 00000000..765baaa8 --- /dev/null +++ b/skimage/util/tests/test_version_requirements.py @@ -0,0 +1,46 @@ +"""Tests for the version requirment functions. + +""" +import numpy as np +from numpy.testing import assert_raises, assert_equal +import nose +from skimage.util import version_requirements as vr + + +def test_get_module_version(): + assert vr.get_module_version('numpy') + assert vr.get_module_version('scipy') + assert_raises(ImportError, lambda: vr.get_module_version('fakenumpy')) + + +def test_is_installed(): + assert vr.is_installed('python', '>=2.6') + assert not vr.is_installed('numpy', '<1.0') + + +def test_require(): + + @vr.require('python', '>2.6') + @vr.require('numpy', '>1.5') + def foo(): + return 1 + + assert_equal(foo(), 1) + + @vr.require('scipy', '<0.1') + def bar(): + return 0 + + assert_raises(ImportError, lambda: bar()) + + @vr.require('numpy', '<1.0') + def test_this(): + assert False + + assert_raises(nose.SkipTest, lambda: test_this()()) + + +def test_get_module(): + assert_equal(vr.get_module('numpy'), np) + assert_equal(vr.get_module('nose'), nose) + diff --git a/skimage/util/version_requirements.py b/skimage/util/version_requirements.py new file mode 100644 index 00000000..2858aa5d --- /dev/null +++ b/skimage/util/version_requirements.py @@ -0,0 +1,125 @@ +from distutils.version import LooseVersion +import functools +import re +import sys +import types +from numpy.testing.decorators import skipif + + +def _check_version(actver, version, cmp_op): + """ + Check version string of an active module against a required version. + + If dev/prerelease tags result in TypeError for string-number comparison, + it is assumed that the dependency is satisfied. + Users on dev branches are responsible for keeping their own packages up to + date. + + Copyright (C) 2013 The IPython Development Team + + Distributed under the terms of the BSD License. + """ + try: + if cmp_op == '>': + return LooseVersion(actver) > LooseVersion(version) + elif cmp_op == '>=': + return LooseVersion(actver) >= LooseVersion(version) + elif cmp_op == '=': + return LooseVersion(actver) == LooseVersion(version) + elif cmp_op == '<': + return LooseVersion(actver) < LooseVersion(version) + else: + return False + except TypeError: + return True + + +def get_module_version(module_name): + """Return module version or None if version can't be retrieved.""" + mod = __import__(module_name, + fromlist=[module_name.rpartition('.')[-1]]) + return getattr(mod, '__version__', getattr(mod, 'VERSION', None)) + + +def is_installed(name, version=None): + """Test if *name* is installed. + + Parameters + ---------- + name : str + Name of module or "python" + version : str, optional + Version string to test against. + If version is not None, checking version + (must have an attribute named '__version__' or 'VERSION') + Version may start with =, >=, > or < to specify the exact requirement + + Note + ---- + Original Copyright (C) 2009-2011 Pierre Raybaut + Licensed under the terms of the MIT License. + + Returns + ------- + True if *name* is installed matching the optional version. + """ + if name.lower() == 'python': + actver = sys.version[:6] + else: + try: + actver = get_module_version(name) + except ImportError: + return False + if version is None: + return True + else: + match = re.search('[0-9]', version) + assert match is not None, "Invalid version number" + symb = version[:match.start()] + if not symb: + symb = '=' + assert symb in ('>=', '>', '=', '<'),\ + "Invalid version condition '%s'" % symb + version = version[match.start():] + return _check_version(actver, version, symb) + + +def require(name, version=None): + """Return decorator that forces a requirement for a function or class. + + Parameters + ---------- + name : str + Name of module or "python". + version : str, optional + Version string to test against. + If version is not None, checking version + (must have an attribute named '__version__' or 'VERSION') + Version may start with =, >=, > or < to specify the exact requirement + """ + def decorator(obj): + @functools.wraps(obj) + def func_wrapped(*args, **kwargs): + if is_installed(name, version): + return obj(*args, **kwargs) + else: + msg = '"%s" in "%s" requires "%s' + msg = msg % (obj, obj.__module__, name) + if not version is None: + msg += " %s" % version + raise ImportError(msg + '"') + if (isinstance(obj, types.FunctionType) and + obj.__name__.startswith('test_')): + if not is_installed(name, version): + func_wrapped = lambda: skipif(True)(obj) + else: + func_wrapped = obj + return func_wrapped + return decorator + + +def get_module(module_name, version=None): + if not is_installed(module_name, version): + return None + return __import__(module_name, + fromlist=[module_name.rpartition('.')[-1]]) diff --git a/skimage/viewer/__init__.py b/skimage/viewer/__init__.py index 5eed9589..a146f348 100644 --- a/skimage/viewer/__init__.py +++ b/skimage/viewer/__init__.py @@ -1 +1,11 @@ +from warnings import warn +from skimage.util.version_requirements import is_installed + from .viewers import ImageViewer, CollectionViewer +from .qt import qt_api + +viewer_available = not qt_api is None and is_installed('matplotlib') +if not viewer_available: + warn('Viewer requires matplotlib and Qt') + +del qt_api, is_installed, warn diff --git a/skimage/viewer/canvastools/base.py b/skimage/viewer/canvastools/base.py index 7f61ce2c..29e6099d 100644 --- a/skimage/viewer/canvastools/base.py +++ b/skimage/viewer/canvastools/base.py @@ -1,10 +1,8 @@ import numpy as np - try: from matplotlib import lines except ImportError: - print("Could not import matplotlib -- skimage.viewer not available.") - + pass __all__ = ['CanvasToolBase', 'ToolHandles'] diff --git a/skimage/viewer/canvastools/linetool.py b/skimage/viewer/canvastools/linetool.py index fed18383..61d94825 100644 --- a/skimage/viewer/canvastools/linetool.py +++ b/skimage/viewer/canvastools/linetool.py @@ -3,8 +3,7 @@ import numpy as np try: from matplotlib import lines except ImportError: - print("Could not import matplotlib -- skimage.viewer not available.") - + pass from skimage.viewer.canvastools.base import CanvasToolBase, ToolHandles diff --git a/skimage/viewer/canvastools/painttool.py b/skimage/viewer/canvastools/painttool.py index 6af8b5d3..fd994a10 100644 --- a/skimage/viewer/canvastools/painttool.py +++ b/skimage/viewer/canvastools/painttool.py @@ -1,9 +1,11 @@ import numpy as np -import matplotlib.pyplot as plt -import matplotlib.colors as mcolors -LABELS_CMAP = mcolors.ListedColormap(['white', 'red', 'dodgerblue', 'gold', - 'greenyellow', 'blueviolet']) - +try: + import matplotlib.pyplot as plt + import matplotlib.colors as mcolors + LABELS_CMAP = mcolors.ListedColormap(['white', 'red', 'dodgerblue', 'gold', + 'greenyellow', 'blueviolet']) +except ImportError: + pass from skimage.viewer.canvastools.base import CanvasToolBase diff --git a/skimage/viewer/canvastools/recttool.py b/skimage/viewer/canvastools/recttool.py index f055e439..929fe939 100644 --- a/skimage/viewer/canvastools/recttool.py +++ b/skimage/viewer/canvastools/recttool.py @@ -2,7 +2,6 @@ try: from matplotlib.widgets import RectangleSelector except ImportError: RectangleSelector = object - print("Could not import matplotlib -- skimage.viewer not available.") from skimage.viewer.canvastools.base import CanvasToolBase from skimage.viewer.canvastools.base import ToolHandles diff --git a/skimage/viewer/plugins/color_histogram.py b/skimage/viewer/plugins/color_histogram.py index dd54a93d..4826fc44 100644 --- a/skimage/viewer/plugins/color_histogram.py +++ b/skimage/viewer/plugins/color_histogram.py @@ -1,6 +1,8 @@ import numpy as np -import matplotlib.pyplot as plt - +try: + import matplotlib.pyplot as plt +except ImportError: + pass from skimage import color from skimage import exposure from .plotplugin import PlotPlugin diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index 373d6bf4..1850992d 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -5,17 +5,12 @@ from .base import Plugin from ..utils import ClearColormap, update_axes_image import six +from skimage.util.version_requirements import is_installed __all__ = ['OverlayPlugin'] -def recent_mpl_version(): - import matplotlib - version = matplotlib.__version__.split('.') - return int(version[0]) == 1 and int(version[1]) >= 2 - - class OverlayPlugin(Plugin): """Plugin for ImageViewer that displays an overlay on top of main image. @@ -38,14 +33,14 @@ class OverlayPlugin(Plugin): 'cyan': (0, 1, 1)} def __init__(self, **kwargs): - if not recent_mpl_version(): + if not is_installed('matplotlib', '>=1.2'): msg = "Matplotlib >= 1.2 required for OverlayPlugin." warn(RuntimeWarning(msg)) super(OverlayPlugin, self).__init__(**kwargs) self._overlay_plot = None self._overlay = None self.cmap = None - self.color_names = list(self.colors.keys()) + self.color_names = sorted(list(self.colors.keys())) def attach(self, image_viewer): super(OverlayPlugin, self).attach(image_viewer) diff --git a/skimage/viewer/tests/test_plugins.py b/skimage/viewer/tests/test_plugins.py index 7ef62730..4708db6e 100644 --- a/skimage/viewer/tests/test_plugins.py +++ b/skimage/viewer/tests/test_plugins.py @@ -4,21 +4,15 @@ import skimage import skimage.data as data from skimage.filter.rank import median from skimage.morphology import disk +from skimage.viewer import ImageViewer, viewer_available from numpy.testing import assert_equal, assert_allclose, assert_almost_equal from numpy.testing.decorators import skipif - -try: - from skimage.viewer.qt import qt_api - from skimage.viewer import ImageViewer - from skimage.viewer.plugins import ( - LineProfile, Measure, CannyPlugin, LabelPainter, Crop, ColorHistogram, - PlotPlugin) - from skimage.viewer.plugins.base import Plugin - from skimage.viewer.widgets import Slider - viewer_available = not qt_api is None -except ImportError: - viewer_available = False - +from skimage.viewer.plugins import ( + LineProfile, Measure, CannyPlugin, LabelPainter, Crop, ColorHistogram, + PlotPlugin) +from skimage.viewer.plugins.base import Plugin +from skimage.viewer.widgets import Slider +from skimage.util.version_requirements import is_installed def setup_line_profile(image, limits='image'): viewer = ImageViewer(skimage.img_as_float(image)) diff --git a/skimage/viewer/tests/test_tools.py b/skimage/viewer/tests/test_tools.py index 9f02e762..6663c465 100644 --- a/skimage/viewer/tests/test_tools.py +++ b/skimage/viewer/tests/test_tools.py @@ -1,19 +1,15 @@ from collections import namedtuple import numpy as np -from skimage import data from numpy.testing import assert_equal from numpy.testing.decorators import skipif - -try: - from skimage.viewer.qt import qt_api - from skimage.viewer import ImageViewer - from skimage.viewer.canvastools import ( - LineTool, ThickLineTool, RectangleTool, PaintTool) - from skimage.viewer.canvastools.base import CanvasToolBase - viewer_available = not qt_api is None -except ImportError: - viewer_available = False +from skimage import data +from skimage.viewer import ImageViewer, viewer_available +from skimage.viewer.canvastools import ( + LineTool, ThickLineTool, RectangleTool, PaintTool) +from skimage.viewer.canvastools.base import CanvasToolBase +from numpy.testing import assert_equal +from numpy.testing.decorators import skipif def get_end_points(image): diff --git a/skimage/viewer/tests/test_utils.py b/skimage/viewer/tests/test_utils.py index 30d74a51..3f9ec396 100644 --- a/skimage/viewer/tests/test_utils.py +++ b/skimage/viewer/tests/test_utils.py @@ -1,15 +1,14 @@ # -*- coding: utf-8 -*- +from skimage.viewer import viewer_available +from skimage.viewer.qt import QtCore, QtGui +from skimage.viewer import utils +from skimage.viewer.utils import dialogs from numpy.testing.decorators import skipif - -try: - from skimage.viewer.qt import qt_api, QtCore, QtGui - from skimage.viewer import utils - from skimage.viewer.utils import dialogs -except ImportError: - qt_api = None +from skimage.viewer import utils +from skimage.viewer.utils import dialogs -@skipif(qt_api is None) +@skipif(not viewer_available) def test_event_loop(): utils.init_qtapp() timer = QtCore.QTimer() @@ -17,7 +16,7 @@ def test_event_loop(): utils.start_qtapp() -@skipif(qt_api is None) +@skipif(not viewer_available) def test_format_filename(): fname = dialogs._format_filename(('apple', 2)) assert fname == 'apple' @@ -25,7 +24,7 @@ def test_format_filename(): assert fname is None -@skipif(qt_api is None) +@skipif(not viewer_available) def test_open_file_dialog(): utils.init_qtapp() timer = QtCore.QTimer() @@ -34,7 +33,7 @@ def test_open_file_dialog(): assert filename is None -@skipif(qt_api is None) +@skipif(not viewer_available) def test_save_file_dialog(): utils.init_qtapp() timer = QtCore.QTimer() diff --git a/skimage/viewer/tests/test_viewer.py b/skimage/viewer/tests/test_viewer.py index cd6c7acb..85a0d76f 100644 --- a/skimage/viewer/tests/test_viewer.py +++ b/skimage/viewer/tests/test_viewer.py @@ -1,18 +1,13 @@ from skimage import data +from skimage.viewer.qt import QtGui, QtCore +from skimage.viewer import ImageViewer, CollectionViewer, viewer_available from skimage.transform import pyramid_gaussian +from skimage.viewer.plugins import OverlayPlugin from skimage.filter import sobel from numpy.testing import assert_equal from numpy.testing.decorators import skipif - -try: - from skimage.viewer.qt import qt_api, QtGui, QtCore - from skimage.viewer.plugins import OverlayPlugin - from skimage.viewer.plugins.overlayplugin import recent_mpl_version - from skimage.viewer import ImageViewer, CollectionViewer - viewer_available = not qt_api is None -except ImportError: - viewer_available = False +from skimage.util.version_requirements import require @skipif(not viewer_available) @@ -58,7 +53,8 @@ def test_collection_viewer(): view._format_coord(10, 10) -@skipif(not viewer_available or not recent_mpl_version()) +@skipif(not viewer_available) +@require('matplotlib', '>=1.2') def test_viewer_with_overlay(): img = data.coins() ov = OverlayPlugin(image_filter=sobel) @@ -68,7 +64,7 @@ def test_viewer_with_overlay(): import tempfile _, filename = tempfile.mkstemp(suffix='.png') - ov.color = 2 + ov.color = 3 assert_equal(ov.color, 'yellow') viewer.save_to_file(filename) ov.display_filtered_image(img) @@ -79,5 +75,3 @@ def test_viewer_with_overlay(): assert_equal(ov.overlay, img) assert_equal(ov.filtered_image, img) - - diff --git a/skimage/viewer/tests/test_widgets.py b/skimage/viewer/tests/test_widgets.py index aeb4ede2..ef3cb83c 100644 --- a/skimage/viewer/tests/test_widgets.py +++ b/skimage/viewer/tests/test_widgets.py @@ -1,19 +1,14 @@ import os from skimage import data, img_as_float, io +from skimage.viewer import ImageViewer, viewer_available +from skimage.viewer.widgets import ( + Slider, OKCancelButtons, SaveButtons, ComboBox, Text) +from skimage.viewer.plugins.base import Plugin +from skimage.viewer.qt import QtGui, QtCore from numpy.testing import assert_almost_equal, assert_equal from numpy.testing.decorators import skipif -try: - from skimage.viewer.qt import qt_api, QtGui, QtCore - from skimage.viewer import ImageViewer - from skimage.viewer.widgets import ( - Slider, OKCancelButtons, SaveButtons, ComboBox, Text) - from skimage.viewer.plugins.base import Plugin - viewer_available = not qt_api is None -except ImportError: - viewer_available = False - def get_image_viewer(): image = data.coins() diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index 95176fcd..19a5c73d 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -9,18 +9,15 @@ try: from matplotlib.figure import Figure from matplotlib import _pylab_helpers from matplotlib.colors import LinearSegmentedColormap - if qt_api is None: - raise ImportError - else: + if not qt_api is None: from matplotlib.backends.backend_qt4 import FigureManagerQT from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg - if 'agg' not in mpl.get_backend().lower(): - print("Recommended matplotlib backend is `Agg` for full " - "skimage.viewer functionality.") + if 'agg' not in mpl.get_backend().lower(): + print("Recommended matplotlib backend is `Agg` for full " + "skimage.viewer functionality.") except ImportError: FigureCanvasQTAgg = object # hack to prevent nosetest and autodoc errors LinearSegmentedColormap = object - print("Could not import matplotlib -- skimage.viewer not available.") from ..qt import QtGui diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 476017ef..52ac073a 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -123,7 +123,6 @@ class ImageViewer(QtGui.QMainWindow): self.fig, self.ax = utils.figimage(image) self.canvas = self.fig.canvas self.canvas.setParent(self) - self.ax.autoscale(enable=False) self._image_plot = self.ax.images[0] diff --git a/tools/build_versions.py b/tools/build_versions.py index 2c0e32ba..5e33dbfa 100755 --- a/tools/build_versions.py +++ b/tools/build_versions.py @@ -4,9 +4,12 @@ from __future__ import print_function import numpy as np import scipy as sp -import matplotlib as mpl +from PIL import Image import six -for m in (np, sp, mpl, six): - print(m.__name__.rjust(10), ' ', m.__version__) - +for m in (np, sp, Image, six): + if not m is None: + if m is Image: + print('PIL'.rjust(10), ' ', Image.VERSION) + else: + print(m.__name__.rjust(10), ' ', m.__version__) From e3b84edf1ba1e36dd191bcb0b4b802514fb91ee9 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 2 Aug 2014 10:30:32 -0500 Subject: [PATCH 0173/1122] Fix python 2.6 restoration/unwrap error --- .travis.yml | 7 +------ skimage/restoration/tests/test_unwrap.py | 7 ------- skimage/restoration/unwrap.py | 3 +-- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1ef5b85d..486c5b60 100644 --- a/.travis.yml +++ b/.travis.yml @@ -90,18 +90,13 @@ script: # Run all doc examples - export PYTHONPATH=$(pwd):$PYTHONPATH - - if [[ $ENV != python=2.6* ]]; then - for f in doc/examples/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done - fi + - for f in doc/examples/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done - for f in doc/examples/applications/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done # run tests again with optional dependencies to get more coverage # measure coverage on py3.3 - # doctests fail on python 2.6 for restoration/unwrap - if [[ $ENV == python=3.3* ]]; then nosetests --exe -v --with-doctest --with-cov --cover-package skimage; - elif [[ $ENV == python=2.6* ]]; then - nosetests --exe -v skimage; else nosetests --exe -v --with-doctest skimage; fi diff --git a/skimage/restoration/tests/test_unwrap.py b/skimage/restoration/tests/test_unwrap.py index 2c2381e3..fca63724 100644 --- a/skimage/restoration/tests/test_unwrap.py +++ b/skimage/restoration/tests/test_unwrap.py @@ -7,7 +7,6 @@ from numpy.testing import (run_module_suite, assert_array_almost_equal, import warnings from skimage.restoration import unwrap_phase -from skimage.util.version_requirements import require def assert_phase_almost_equal(a, b, *args, **kwargs): @@ -42,7 +41,6 @@ def check_unwrap(image, mask=None): assert_phase_almost_equal(image_unwrapped, image) -@require('python', '>=2.7') def test_unwrap_1d(): image = np.linspace(0, 10 * np.pi, 100) check_unwrap(image) @@ -52,7 +50,6 @@ def test_unwrap_1d(): assert_raises(ValueError, unwrap_phase, image, True) -@require('python', '>=2.7') def test_unwrap_2d(): x, y = np.ogrid[:8, :16] image = 2 * np.pi * (x * 0.2 + y * 0.1) @@ -62,7 +59,6 @@ def test_unwrap_2d(): yield check_unwrap, image, mask -@require('python', '>=2.7') def test_unwrap_3d(): x, y, z = np.ogrid[:8, :12, :16] image = 2 * np.pi * (x * 0.2 + y * 0.1 + z * 0.05) @@ -108,14 +104,12 @@ def check_wrap_around(ndim, axis): image_unwrap_wrap_around[index_last]) -@require('python', '>=2.7') def test_wrap_around(): for ndim in (2, 3): for axis in range(ndim): yield check_wrap_around, ndim, axis -@require('python', '>=2.7') def test_mask(): length = 100 ramps = [np.linspace(0, 4 * np.pi, length), @@ -143,7 +137,6 @@ def test_mask(): assert_array_almost_equal(image_unwrapped_3d[:, :, -1], image[i, -1]) -@require('python', '>=2.7') def test_invalid_input(): assert_raises(ValueError, unwrap_phase, np.zeros([])) assert_raises(ValueError, unwrap_phase, np.zeros((1, 1, 1, 1))) diff --git a/skimage/restoration/unwrap.py b/skimage/restoration/unwrap.py index 14da2677..f0025775 100644 --- a/skimage/restoration/unwrap.py +++ b/skimage/restoration/unwrap.py @@ -1,14 +1,12 @@ import numpy as np import warnings from six import string_types -from skimage.util.version_requirements import require from ._unwrap_1d import unwrap_1d from ._unwrap_2d import unwrap_2d from ._unwrap_3d import unwrap_3d -@require('python', '>=2.7') def unwrap_phase(image, wrap_around=False): '''From ``image``, wrapped to lie in the interval [-pi, pi), recover the original, unwrapped image. @@ -86,6 +84,7 @@ def unwrap_phase(image, wrap_around=False): if np.ma.isMaskedArray(image): mask = np.require(image.mask, np.uint8, ['C']) + image = image.data else: mask = np.zeros_like(image, dtype=np.uint8, order='C') image_not_masked = np.asarray(image, dtype=np.float64, order='C') From 208afc38371941f257c2b713481f1f92486654c0 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 2 Aug 2014 11:09:46 -0500 Subject: [PATCH 0174/1122] Move version_requirements to _shared and remove skipif from require. --- .../{util => _shared}/tests/test_version_requirements.py | 8 +------- skimage/{util => _shared}/version_requirements.py | 7 ------- skimage/graph/tests/test_rag.py | 7 ++++--- skimage/viewer/__init__.py | 2 +- skimage/viewer/plugins/overlayplugin.py | 2 +- skimage/viewer/tests/test_plugins.py | 2 +- skimage/viewer/tests/test_viewer.py | 4 ++-- 7 files changed, 10 insertions(+), 22 deletions(-) rename skimage/{util => _shared}/tests/test_version_requirements.py (81%) rename skimage/{util => _shared}/version_requirements.py (92%) diff --git a/skimage/util/tests/test_version_requirements.py b/skimage/_shared/tests/test_version_requirements.py similarity index 81% rename from skimage/util/tests/test_version_requirements.py rename to skimage/_shared/tests/test_version_requirements.py index 765baaa8..9511a1fe 100644 --- a/skimage/util/tests/test_version_requirements.py +++ b/skimage/_shared/tests/test_version_requirements.py @@ -4,7 +4,7 @@ import numpy as np from numpy.testing import assert_raises, assert_equal import nose -from skimage.util import version_requirements as vr +from skimage._shared import version_requirements as vr def test_get_module_version(): @@ -33,12 +33,6 @@ def test_require(): assert_raises(ImportError, lambda: bar()) - @vr.require('numpy', '<1.0') - def test_this(): - assert False - - assert_raises(nose.SkipTest, lambda: test_this()()) - def test_get_module(): assert_equal(vr.get_module('numpy'), np) diff --git a/skimage/util/version_requirements.py b/skimage/_shared/version_requirements.py similarity index 92% rename from skimage/util/version_requirements.py rename to skimage/_shared/version_requirements.py index 2858aa5d..7407d600 100644 --- a/skimage/util/version_requirements.py +++ b/skimage/_shared/version_requirements.py @@ -3,7 +3,6 @@ import functools import re import sys import types -from numpy.testing.decorators import skipif def _check_version(actver, version, cmp_op): @@ -108,12 +107,6 @@ def require(name, version=None): if not version is None: msg += " %s" % version raise ImportError(msg + '"') - if (isinstance(obj, types.FunctionType) and - obj.__name__.startswith('test_')): - if not is_installed(name, version): - func_wrapped = lambda: skipif(True)(obj) - else: - func_wrapped = obj return func_wrapped return decorator diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index b6f95cb2..753bcb74 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -1,6 +1,7 @@ import numpy as np from skimage import graph -from skimage.util.version_requirements import require +from skimage._shared.version_requirements import is_installed +from numpy.testing.decorators import skipif def max_edge(g, src, dst, n): @@ -10,7 +11,7 @@ def max_edge(g, src, dst, n): return max(w1, w2) -@require('networkx') +@skipif(not is_installed('networkx')) def test_rag_merge(): g = graph.rag.RAG() @@ -45,7 +46,7 @@ def test_rag_merge(): assert g.edges() == [] -@require('networkx') +@skipif(not is_installed('networkx')) def test_threshold_cut(): img = np.zeros((100, 100, 3), dtype='uint8') diff --git a/skimage/viewer/__init__.py b/skimage/viewer/__init__.py index a146f348..4bf70a93 100644 --- a/skimage/viewer/__init__.py +++ b/skimage/viewer/__init__.py @@ -1,5 +1,5 @@ from warnings import warn -from skimage.util.version_requirements import is_installed +from skimage._shared.version_requirements import is_installed from .viewers import ImageViewer, CollectionViewer from .qt import qt_api diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index 1850992d..7f016328 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -5,7 +5,7 @@ from .base import Plugin from ..utils import ClearColormap, update_axes_image import six -from skimage.util.version_requirements import is_installed +from skimage._shared.version_requirements import is_installed __all__ = ['OverlayPlugin'] diff --git a/skimage/viewer/tests/test_plugins.py b/skimage/viewer/tests/test_plugins.py index 4708db6e..497708a4 100644 --- a/skimage/viewer/tests/test_plugins.py +++ b/skimage/viewer/tests/test_plugins.py @@ -12,7 +12,7 @@ from skimage.viewer.plugins import ( PlotPlugin) from skimage.viewer.plugins.base import Plugin from skimage.viewer.widgets import Slider -from skimage.util.version_requirements import is_installed + def setup_line_profile(image, limits='image'): viewer = ImageViewer(skimage.img_as_float(image)) diff --git a/skimage/viewer/tests/test_viewer.py b/skimage/viewer/tests/test_viewer.py index 85a0d76f..1b93ff52 100644 --- a/skimage/viewer/tests/test_viewer.py +++ b/skimage/viewer/tests/test_viewer.py @@ -7,7 +7,7 @@ from skimage.viewer.plugins import OverlayPlugin from skimage.filter import sobel from numpy.testing import assert_equal from numpy.testing.decorators import skipif -from skimage.util.version_requirements import require +from skimage._shared.version_requirements import is_installed @skipif(not viewer_available) @@ -54,7 +54,7 @@ def test_collection_viewer(): @skipif(not viewer_available) -@require('matplotlib', '>=1.2') +@skipif(not is_installed('matplotlib', '>=1.2')) def test_viewer_with_overlay(): img = data.coins() ov = OverlayPlugin(image_filter=sobel) From f2b7e104f8b1056fef70c328d43c7b9c1f55bd82 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 4 Aug 2014 06:37:48 -0500 Subject: [PATCH 0175/1122] Add documentation and rename import variable --- .travis.yml | 2 ++ .../tests/test_version_requirements.py | 26 ++++++++++--------- skimage/_shared/version_requirements.py | 20 ++++++++++++++ 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 486c5b60..4633a395 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,8 @@ before_install: - sh -e /etc/init.d/xvfb start - sudo apt-get update + # Python 3.2 is not supported by Miniconda, so we use the package manager for that run. + # NumPy has a bug in python 3 that is only fixed in the latest version, hence the download of _import _tools. - if [[ $ENV == python=3.2 ]]; then sudo apt-get install python3-numpy; wget https://raw.githubusercontent.com/numpy/numpy/master/numpy/_import_tools.py -O /home/travis/virtualenv/python3.2_with_system_site_packages/lib/python3.2/site-packages/numpy/_import_tools.py; diff --git a/skimage/_shared/tests/test_version_requirements.py b/skimage/_shared/tests/test_version_requirements.py index 9511a1fe..7c9bd2fb 100644 --- a/skimage/_shared/tests/test_version_requirements.py +++ b/skimage/_shared/tests/test_version_requirements.py @@ -4,30 +4,33 @@ import numpy as np from numpy.testing import assert_raises, assert_equal import nose -from skimage._shared import version_requirements as vr +from skimage._shared import version_requirements as version_req def test_get_module_version(): - assert vr.get_module_version('numpy') - assert vr.get_module_version('scipy') - assert_raises(ImportError, lambda: vr.get_module_version('fakenumpy')) + assert version_req.get_module_version('numpy') + assert version_req.get_module_version('scipy') + assert_raises(ImportError, + lambda: version_req.get_module_version('fakenumpy')) def test_is_installed(): - assert vr.is_installed('python', '>=2.6') - assert not vr.is_installed('numpy', '<1.0') + assert version_req.is_installed('python', '>=2.6') + assert not version_req.is_installed('numpy', '<1.0') def test_require(): - @vr.require('python', '>2.6') - @vr.require('numpy', '>1.5') + # A function that only runs on Python >2.6 and numpy > 1.5 (should pass) + @version_req.require('python', '>2.6') + @version_req.require('numpy', '>1.5') def foo(): return 1 assert_equal(foo(), 1) - @vr.require('scipy', '<0.1') + # function that requires scipy < 0.1 (should fail) + @version_req.require('scipy', '<0.1') def bar(): return 0 @@ -35,6 +38,5 @@ def test_require(): def test_get_module(): - assert_equal(vr.get_module('numpy'), np) - assert_equal(vr.get_module('nose'), nose) - + assert_equal(version_req.get_module('numpy'), np) + assert_equal(version_req.get_module('nose'), nose) diff --git a/skimage/_shared/version_requirements.py b/skimage/_shared/version_requirements.py index 7407d600..1da9c195 100644 --- a/skimage/_shared/version_requirements.py +++ b/skimage/_shared/version_requirements.py @@ -95,6 +95,10 @@ def require(name, version=None): If version is not None, checking version (must have an attribute named '__version__' or 'VERSION') Version may start with =, >=, > or < to specify the exact requirement + + Returns + ------- + A decorator function. """ def decorator(obj): @functools.wraps(obj) @@ -112,6 +116,22 @@ def require(name, version=None): def get_module(module_name, version=None): + """Return a module object of name *module_name* if installed. + + Parameters + ---------- + module_name : str + Name of module. + version : str, optional + Version string to test against. + If version is not None, checking version + (must have an attribute named '__version__' or 'VERSION') + Version may start with =, >=, > or < to specify the exact requirement + + Returns + ------- + Module if *module_name* is installed matching the optional version or None. + """ if not is_installed(module_name, version): return None return __import__(module_name, From 26a550a93b44ac29e0bc2b78b307048cadbc6f58 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 4 Aug 2014 07:13:42 -0500 Subject: [PATCH 0176/1122] Formatting fix. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4633a395..e21fd201 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ before_install: - sudo apt-get update # Python 3.2 is not supported by Miniconda, so we use the package manager for that run. - # NumPy has a bug in python 3 that is only fixed in the latest version, hence the download of _import _tools. + # NumPy has a bug in python 3 that is only fixed in the latest version, hence the download of _import_tools. - if [[ $ENV == python=3.2 ]]; then sudo apt-get install python3-numpy; wget https://raw.githubusercontent.com/numpy/numpy/master/numpy/_import_tools.py -O /home/travis/virtualenv/python3.2_with_system_site_packages/lib/python3.2/site-packages/numpy/_import_tools.py; From 382be1a45789d85ef27bc85c5fb1e07b206242c3 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 4 Aug 2014 07:26:07 -0500 Subject: [PATCH 0177/1122] Formatting and docstring cleanup. --- .../_shared/tests/test_version_requirements.py | 2 +- skimage/_shared/version_requirements.py | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/skimage/_shared/tests/test_version_requirements.py b/skimage/_shared/tests/test_version_requirements.py index 7c9bd2fb..59cf5814 100644 --- a/skimage/_shared/tests/test_version_requirements.py +++ b/skimage/_shared/tests/test_version_requirements.py @@ -1,4 +1,4 @@ -"""Tests for the version requirment functions. +"""Tests for the version requirement functions. """ import numpy as np diff --git a/skimage/_shared/version_requirements.py b/skimage/_shared/version_requirements.py index 1da9c195..4ebf8ee9 100644 --- a/skimage/_shared/version_requirements.py +++ b/skimage/_shared/version_requirements.py @@ -2,7 +2,6 @@ from distutils.version import LooseVersion import functools import re import sys -import types def _check_version(actver, version, cmp_op): @@ -53,14 +52,15 @@ def is_installed(name, version=None): (must have an attribute named '__version__' or 'VERSION') Version may start with =, >=, > or < to specify the exact requirement + Returns + ------- + out : bool + True if *name* is installed matching the optional version. + Note ---- Original Copyright (C) 2009-2011 Pierre Raybaut Licensed under the terms of the MIT License. - - Returns - ------- - True if *name* is installed matching the optional version. """ if name.lower() == 'python': actver = sys.version[:6] @@ -98,7 +98,9 @@ def require(name, version=None): Returns ------- - A decorator function. + func : function + A decorator that raises an ImportError if a function is run + in the absence of the input dependency. """ def decorator(obj): @functools.wraps(obj) @@ -130,7 +132,9 @@ def get_module(module_name, version=None): Returns ------- - Module if *module_name* is installed matching the optional version or None. + mod : module or None + Module if *module_name* is installed matching the optional version + or None otherwise. """ if not is_installed(module_name, version): return None From 04e53f124f4aecf71320db44ee65d1baf384d24e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 4 Aug 2014 07:30:13 -0500 Subject: [PATCH 0178/1122] Remove confusing dummy variable --- skimage/graph/graph_cut.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 443b1561..d9fcfdef 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -3,7 +3,6 @@ try: except ImportError: import warnings warnings.warn('"cut_threshold" requires networkx') - nx = None import numpy as np From 56152fe6a787139654b060bf7603d30584f0e5b5 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 4 Aug 2014 07:32:11 -0500 Subject: [PATCH 0179/1122] Update comment --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e21fd201..5f127645 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,8 @@ before_install: - sudo apt-get update # Python 3.2 is not supported by Miniconda, so we use the package manager for that run. - # NumPy has a bug in python 3 that is only fixed in the latest version, hence the download of _import_tools. + # NumPy has a bug in python 3 that is only fixed in the latest version, + # hence the below wget of numpy/_import_tools.py from github. - if [[ $ENV == python=3.2 ]]; then sudo apt-get install python3-numpy; wget https://raw.githubusercontent.com/numpy/numpy/master/numpy/_import_tools.py -O /home/travis/virtualenv/python3.2_with_system_site_packages/lib/python3.2/site-packages/numpy/_import_tools.py; From 2a339fe69cbca2a871b5b2425caee417d462a928 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 4 Aug 2014 07:40:37 -0500 Subject: [PATCH 0180/1122] Improve import behavior for rag fallback --- skimage/graph/rag.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index fc4a1aaa..7c1b7f07 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -1,11 +1,13 @@ try: import networkx as nx except ImportError: + msg = "Graph functions require networkx, which is not installed" class nx: class Graph: - pass + def __init__(self, *args, **kwargs): + raise ImportError(msg) import warnings - warnings.warn('Region Adjacency Graph (RAG) features require networkx') + warnings.warn(msg) import numpy as np from scipy.ndimage import filters from scipy import ndimage as nd From 480a9f72fc0326840394310507f3ca284e4c828e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 4 Aug 2014 10:44:23 -0500 Subject: [PATCH 0181/1122] Add note in travis --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5f127645..b0acc3c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,8 @@ language: python + +# Tthe Travis python is set to 3.2 for all builds, since we use the default python for the 3.2 build and the anaconda python otherwise python: - 3.2 @@ -24,7 +26,7 @@ before_install: - sudo apt-get update # Python 3.2 is not supported by Miniconda, so we use the package manager for that run. - # NumPy has a bug in python 3 that is only fixed in the latest version, + # NumPy has a bug in python 3 that is only fixed in the latest version, # hence the below wget of numpy/_import_tools.py from github. - if [[ $ENV == python=3.2 ]]; then sudo apt-get install python3-numpy; @@ -59,7 +61,7 @@ script: - flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples # Install optional dependencies to get full test coverage - # Notes: + # Notes: # - pyfits and imread do NOT support py3.2 # - Use the png headers included in anaconda (from matplotlib install) # TODO: Remove the libm removal when anaconda fixes their libraries From 5996cf8abacddf22af7e0523cca9f1c4be0b46c7 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 4 Aug 2014 10:48:15 -0500 Subject: [PATCH 0182/1122] Add travis_retry around the conda installs --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b0acc3c4..3a061e13 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,7 +41,7 @@ before_install: conda config --set always_yes yes; conda update conda; conda info -a; - conda create -n test $ENV six scipy pip flake8 nose; + travis_retry conda create -n test $ENV six scipy pip flake8 nose; source activate test; fi - pip install coveralls pillow @@ -72,7 +72,7 @@ script: pip install --use-mirrors -q matplotlib; pip install -q networkx; else - conda install -q matplotlib pyqt networkx; + travis_retry conda install -q matplotlib pyqt networkx; sudo cp ~/miniconda/envs/test/include/png* /usr/include; rm ~/miniconda/envs/test/lib/libm-2.5.so; rm ~/miniconda/envs/test/lib/libm.*; From e74ba1b4f006fe9959a27916f0a2caf84078478e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 11 Jun 2014 07:22:10 -0400 Subject: [PATCH 0183/1122] Add release note instructions --- RELEASE.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASE.txt b/RELEASE.txt index b56b5bb8..de90a580 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -8,6 +8,10 @@ How to make a new release of ``skimage`` - To show a list of contributors and changes, run ``doc/release/contribs.py ``. + - Review and cleanup ``doc/release/release_dev.txt``, rename to + ``doc/release/release_X.txt``, and create new + ``doc/release/release_dev.txt``. + - Update the version number in ``setup.py`` and ``bento.info`` and commit - Update the docs: From c3b4e589786d5c82009a27b8281dcef54ecf2cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 11 Jun 2014 07:28:48 -0400 Subject: [PATCH 0184/1122] Describe new release note dev process For upcoming contributions, every contributor should document the contributed changes, before merging the PR. --- CONTRIBUTING.txt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index 202210a1..837dca2a 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -77,6 +77,12 @@ For a more detailed discussion, read these :doc:`detailed documents Travis fails, you can find out why by clicking on the "failed" icon (red cross) and inspecting the build and test log. +5. Document changes + + Before merging your commits, you must add a description of your changes + to the release notes of the upcoming version in + ``doc/release/release_dev.txt``. + .. note:: To reviewers: if it is not obvious, add a short explanation of what a branch @@ -85,7 +91,7 @@ For a more detailed discussion, read these :doc:`detailed documents Divergence between ``upstream master`` and your feature branch -.............................................................. +-------------------------------------------------------------- Do *not* ever merge the main branch into yours. If GitHub indicates that the branch of your Pull Request can no longer be merged automatically, rebase From 4235e8a448883d673d3927c584e38194150bc7b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 11 Jun 2014 07:55:48 -0400 Subject: [PATCH 0185/1122] Add release notes for upcoming versions --- doc/release/release_dev.txt | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 doc/release/release_dev.txt diff --git a/doc/release/release_dev.txt b/doc/release/release_dev.txt new file mode 100644 index 00000000..9503b7e7 --- /dev/null +++ b/doc/release/release_dev.txt @@ -0,0 +1,35 @@ +Announcement: scikit-image 0.X.0 +================================ + +New Features +------------ + + + + +Improvements +------------ + + + + +Changes +------- + + + + +Deprecations +------------ + + + + +Contributors to this release +---------------------------- + +This release was made possible by the collaborative efforts of many +contributors, both new and old. They are listed in alphabetical order by +surname: + + From 5d4ec15fd597996b2b912f7360867cee869a55b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 15 Jun 2014 08:29:55 -0400 Subject: [PATCH 0186/1122] Remove contributors for current release notes --- doc/release/release_dev.txt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/doc/release/release_dev.txt b/doc/release/release_dev.txt index 9503b7e7..da8c2b07 100644 --- a/doc/release/release_dev.txt +++ b/doc/release/release_dev.txt @@ -23,13 +23,3 @@ Deprecations ------------ - - -Contributors to this release ----------------------------- - -This release was made possible by the collaborative efforts of many -contributors, both new and old. They are listed in alphabetical order by -surname: - - From be3ac9a14181d0da29b9e06444b8ad56edfe38e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 4 Aug 2014 21:15:47 -0400 Subject: [PATCH 0187/1122] Rename Changes section to API changes --- doc/release/release_dev.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/release/release_dev.txt b/doc/release/release_dev.txt index da8c2b07..00d34ac3 100644 --- a/doc/release/release_dev.txt +++ b/doc/release/release_dev.txt @@ -13,8 +13,8 @@ Improvements -Changes -------- +API Changes +----------- From 8ef53d04f069be082cffb675d04e471f74a4388f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 4 Aug 2014 22:01:10 -0400 Subject: [PATCH 0188/1122] Add intro to release notes --- doc/release/release_dev.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/release/release_dev.txt b/doc/release/release_dev.txt index 00d34ac3..1123eb77 100644 --- a/doc/release/release_dev.txt +++ b/doc/release/release_dev.txt @@ -1,6 +1,17 @@ Announcement: scikit-image 0.X.0 ================================ +We're happy to announce the release of scikit-image v0.X.0! + +scikit-image is an image processing toolbox for SciPy that includes algorithms +for segmentation, geometric transformations, color space manipulation, +analysis, filtering, morphology, feature detection, and more. + +For more information, examples, and documentation, please visit our website: + +http://scikit-image.org + + New Features ------------ From c3b79ca6781f0b7b438677675e0837d3c29e0e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 4 Aug 2014 22:01:40 -0400 Subject: [PATCH 0189/1122] Add release notes template --- doc/release/release_template.txt | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 doc/release/release_template.txt diff --git a/doc/release/release_template.txt b/doc/release/release_template.txt new file mode 100644 index 00000000..1123eb77 --- /dev/null +++ b/doc/release/release_template.txt @@ -0,0 +1,36 @@ +Announcement: scikit-image 0.X.0 +================================ + +We're happy to announce the release of scikit-image v0.X.0! + +scikit-image is an image processing toolbox for SciPy that includes algorithms +for segmentation, geometric transformations, color space manipulation, +analysis, filtering, morphology, feature detection, and more. + +For more information, examples, and documentation, please visit our website: + +http://scikit-image.org + + +New Features +------------ + + + + +Improvements +------------ + + + + +API Changes +----------- + + + + +Deprecations +------------ + + From 017d051f9440484e7e369846de1b3de4620916f2 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 5 Aug 2014 01:02:07 -0500 Subject: [PATCH 0190/1122] Clarify release procedure doc --- RELEASE.txt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/RELEASE.txt b/RELEASE.txt index de90a580..90e830b9 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -5,12 +5,15 @@ How to make a new release of ``skimage`` - Update release notes. - - To show a list of contributors and changes, run - ``doc/release/contribs.py ``. + 1. Review and cleanup ``doc/release/release_dev.txt`` - - Review and cleanup ``doc/release/release_dev.txt``, rename to - ``doc/release/release_X.txt``, and create new - ``doc/release/release_dev.txt``. + - To show a list of contributors and changes, run + ``doc/release/contribs.py ``. + + 2. Rename to ``doc/release/release_X.txt`` + + 3. Copy ``doc/release/release_template.txt`` to + ``doc/release/release_dev.txt`` for the next release. - Update the version number in ``setup.py`` and ``bento.info`` and commit From b03683d6c53f1f5234964b39cdb9513a36f2e73f Mon Sep 17 00:00:00 2001 From: Adam Feuer Date: Thu, 24 Jul 2014 21:59:06 -0700 Subject: [PATCH 0191/1122] pil_plugin can import and export from memory - added capability to import and export to PIL Image objects, not just files - allows for converting to and from PIL image objects without writing to disk - no unit test yet --- skimage/io/_plugins/pil_plugin.py | 62 ++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index e6f9c491..80104f68 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -21,7 +21,14 @@ def imread(fname, dtype=None): """ im = Image.open(fname) - fp = im.fp + return pil_to_ndarray(im, dtype) + + +def pil_to_ndarray(im, dtype=None): + """import an image from a PIL Image object in memory + + """ + fp = im.fp if hasattr(im, 'fp') else None if im.mode == 'P': if _palette_is_grayscale(im): im = im.convert('L') @@ -37,7 +44,8 @@ def imread(fname, dtype=None): elif 'A' in im.mode: im = im.convert('RGBA') im = np.array(im, dtype=dtype) - fp.close() + if fp: + fp.close() return im @@ -65,24 +73,12 @@ def _palette_is_grayscale(pil_image): return np.allclose(np.diff(valid_palette), 0) -def imsave(fname, arr, format_str=None): - """Save an image to disk. +def ndarray_to_pil(arr, format_str=None): + """Export an image as PIL object Parameters ---------- - fname : str or file-like object - Name of destination file. - arr : ndarray of uint8 or float - Array (image) to save. Arrays of data-type uint8 should have - values in [0, 255], whereas floating-point arrays must be - in [0, 1]. - format_str: str - Format to save as, this is defaulted to PNG if using a file-like - object; this will be derived from the extension if fname is a string - - Notes - ----- - Currently, only 8-bit precision is supported. + See imsave """ arr = np.asarray(arr).squeeze() @@ -109,10 +105,6 @@ def imsave(fname, arr, format_str=None): # Force all integers to bytes arr = arr.astype(np.uint8) - # default to PNG if file-like object - if not isinstance(fname, string_types) and format_str is None: - format_str = "PNG" - try: img = Image.frombytes(mode, (arr.shape[1], arr.shape[0]), arr.tostring()) @@ -120,6 +112,34 @@ def imsave(fname, arr, format_str=None): img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), arr.tostring()) + return img + + +def imsave(fname, arr, format_str=None): + """Save an image to disk. + + Parameters + ---------- + fname : str or file-like object + Name of destination file. + arr : ndarray of uint8 or float + Array (image) to save. Arrays of data-type uint8 should have + values in [0, 255], whereas floating-point arrays must be + in [0, 1]. + format_str: str + Format to save as, this is defaulted to PNG if using a file-like + object; this will be derived from the extension if fname is a string + + Notes + ----- + Currently, only 8-bit precision is supported. + + """ + # default to PNG if file-like object + if not isinstance(fname, string_types) and format_str is None: + format_str = "PNG" + + img = ndarray_to_pil(arr, format_str=None) img.save(fname, format=format_str) From 1dcb60fd68fdf20b420d6d14b2549eba3d4f8781 Mon Sep 17 00:00:00 2001 From: Adam Feuer Date: Thu, 24 Jul 2014 22:49:57 -0700 Subject: [PATCH 0192/1122] added tests for roundtrip export and import - and corrected error in imexport - wasn't using imsave code correctly - split imsave into imsave and imexport functions to reuse imsave code --- skimage/io/_plugins/pil_plugin.py | 1 + skimage/io/tests/test_pil.py | 35 +++++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 80104f68..9510f4d0 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -115,6 +115,7 @@ def ndarray_to_pil(arr, format_str=None): return img + def imsave(fname, arr, format_str=None): """Save an image to disk. diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index c5718388..e5e7647c 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -14,7 +14,7 @@ from six import BytesIO try: from PIL import Image - from skimage.io._plugins.pil_plugin import _palette_is_grayscale + from skimage.io._plugins.pil_plugin import imimport, imexport, _palette_is_grayscale use_plugin('pil') except ImportError: PIL_available = False @@ -113,26 +113,40 @@ def test_imread_uint16_big_endian(): class TestSave: - def roundtrip(self, dtype, x, scaling=1): + def roundtrip_file(self, x): f = NamedTemporaryFile(suffix='.png') fname = f.name f.close() imsave(fname, x) y = imread(fname) + return y + def roundtrip_pil_image(self, x): + pil_image = imexport(x) + y = imimport(pil_image) + return y + + def verify_roundtrip(self, dtype, x, y, scaling=1): assert_array_almost_equal((x * scaling).astype(np.int32), y) - @skipif(not PIL_available) - def test_imsave_roundtrip(self): + def verify_imsave_roundtrip(self, roundtrip_function): for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: for dtype in (np.uint8, np.uint16, np.float32, np.float64): x = np.ones(shape, dtype=dtype) * np.random.rand(*shape) if np.issubdtype(dtype, float): - yield self.roundtrip, dtype, x, 255 + yield self.verify_roundtrip, dtype, x, roundtrip_function(x), 255 else: x = (x * 255).astype(dtype) - yield self.roundtrip, dtype, x + yield self.verify_roundtrip, dtype, x, roundtrip_function(x) + + @skipif(not PIL_available) + def test_imsave_roundtrip_file(self): + self.verify_imsave_roundtrip(self.roundtrip_file) + + @skipif(not PIL_available) + def test_imsave_roundtrip_pil_image(self): + self.verify_imsave_roundtrip(self.roundtrip_pil_image) @skipif(not PIL_available) @@ -151,5 +165,14 @@ def test_imsave_filelike(): assert_allclose(out, image) +@skipif(not PIL_available) +def test_imexport_imimport(): + shape = (2, 2) + image = np.zeros(shape) + pil_image = imexport(image) + out = imimport(pil_image) + assert out.shape == shape + + if __name__ == "__main__": run_module_suite() From 32061fb66d49f27e78ce15a639e608f939c61e18 Mon Sep 17 00:00:00 2001 From: Adam Feuer Date: Thu, 24 Jul 2014 22:59:14 -0700 Subject: [PATCH 0193/1122] added a space character to trigger travis build --- skimage/io/tests/test_pil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index e5e7647c..b84bf074 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -167,7 +167,7 @@ def test_imsave_filelike(): @skipif(not PIL_available) def test_imexport_imimport(): - shape = (2, 2) + shape = (2, 2) image = np.zeros(shape) pil_image = imexport(image) out = imimport(pil_image) From 4a0b99ad6ff2d5fd7e87a80b752598e2a8646c48 Mon Sep 17 00:00:00 2001 From: Adam Feuer Date: Fri, 25 Jul 2014 07:10:50 -0700 Subject: [PATCH 0194/1122] fixed bad indent --- skimage/io/tests/test_pil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index b84bf074..e5e7647c 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -167,7 +167,7 @@ def test_imsave_filelike(): @skipif(not PIL_available) def test_imexport_imimport(): - shape = (2, 2) + shape = (2, 2) image = np.zeros(shape) pil_image = imexport(image) out = imimport(pil_image) From 1cb73c79d65fb3f093b92321fed741eea190b93e Mon Sep 17 00:00:00 2001 From: Adam Feuer Date: Sun, 3 Aug 2014 10:46:06 -0700 Subject: [PATCH 0195/1122] improved name of pil import/export functions - to make it clear what the functions actually do: ndarray_to_pil() and pil_to_ndarray() - as per code review feedback - updated tests to use the new function names - incoming change to pil_plugin assumed PIL Image objects will always be from files; this is not always true anymore. Now pil_plugin checks to see if the Image came from a file before trying to close its associated file. --- skimage/io/tests/test_pil.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index e5e7647c..5cd36f92 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -14,7 +14,7 @@ from six import BytesIO try: from PIL import Image - from skimage.io._plugins.pil_plugin import imimport, imexport, _palette_is_grayscale + from skimage.io._plugins.pil_plugin import pil_to_ndarray, ndarray_to_pil, _palette_is_grayscale use_plugin('pil') except ImportError: PIL_available = False @@ -122,8 +122,8 @@ class TestSave: return y def roundtrip_pil_image(self, x): - pil_image = imexport(x) - y = imimport(pil_image) + pil_image = ndarray_to_pil(x) + y = pil_to_ndarray(pil_image) return y def verify_roundtrip(self, dtype, x, y, scaling=1): @@ -169,8 +169,8 @@ def test_imsave_filelike(): def test_imexport_imimport(): shape = (2, 2) image = np.zeros(shape) - pil_image = imexport(image) - out = imimport(pil_image) + pil_image = ndarray_to_pil(image) + out = pil_to_ndarray(pil_image) assert out.shape == shape From 7215936f5c39a85ba43cb0b93574141c7e6e69f3 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 23 Jul 2014 02:14:46 +0530 Subject: [PATCH 0196/1122] First working copy --- skimage/graph/__init__.py | 3 +- skimage/graph/_ncut.py | 38 +++++++++++++++++++++ skimage/graph/_ncut_cy.pyx | 27 +++++++++++++++ skimage/graph/graph_cut.py | 70 +++++++++++++++++++++++++++++++++++++- skimage/graph/rag.py | 49 ++++++++++++++++++++++++++ skimage/graph/setup.py | 4 ++- 6 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 skimage/graph/_ncut.py create mode 100644 skimage/graph/_ncut_cy.pyx diff --git a/skimage/graph/__init__.py b/skimage/graph/__init__.py index 68c1206c..9fe00d3c 100644 --- a/skimage/graph/__init__.py +++ b/skimage/graph/__init__.py @@ -1,7 +1,7 @@ from .spath import shortest_path from .mcp import MCP, MCP_Geometric, MCP_Connect, MCP_Flexible, route_through_array from .rag import rag_mean_color, RAG -from .graph_cut import cut_threshold +from .graph_cut import cut_threshold, cut_n __all__ = ['shortest_path', 'MCP', @@ -11,4 +11,5 @@ __all__ = ['shortest_path', 'route_through_array', 'rag_mean_color', 'cut_threshold', + 'cut_n', 'RAG'] diff --git a/skimage/graph/_ncut.py b/skimage/graph/_ncut.py new file mode 100644 index 00000000..9b321bd8 --- /dev/null +++ b/skimage/graph/_ncut.py @@ -0,0 +1,38 @@ +import networkx as nx +import numpy as np +from scipy import sparse + +def DW_matrix(graph): + #print graph[4][4]['weight'] + W = nx.to_scipy_sparse_matrix(graph, format='csc') + entries = W.sum(0) + D = sparse.dia_matrix( (entries,0),shape = W.shape).tocsc() + #print W[4,4] + + m,n = W.shape + #for i in range(n): + # W[i,i] = 1.0 + return D,W + +def ncut_cost(mask,D,W): + + mask = np.array(mask) + mask_list = [ np.logical_xor(mask[i], mask) for i in range(mask.shape[0])] + mask_array = np.array(mask_list) + + cut = float(W[mask_array].sum()/2.0) + #print W.todense() + #print mask_array.astype(int) + #print "cut=",cut + + assoc_a = D.data[mask].sum() + assoc_b = D.data[np.logical_not(mask)].sum() + + #print cut + #print assoc_a,assoc_b + return (cut/assoc_a) + (cut/assoc_b) + +def norml(a): + mi = a.min() + mx = a.max() + return (a-mi)/(mx-mi) diff --git a/skimage/graph/_ncut_cy.pyx b/skimage/graph/_ncut_cy.pyx new file mode 100644 index 00000000..fc3c0f3b --- /dev/null +++ b/skimage/graph/_ncut_cy.pyx @@ -0,0 +1,27 @@ +# cython: cdivision=True +# cython: boundscheck=False +# cython: nonecheck=False +# cython: wraparound=False +cimport numpy as cnp +import numpy as np + +def argmin2(cnp.float64_t[:] array): + cdef cnp.float64_t min1 = np.inf + cdef cnp.float64_t min2 = np.inf + cdef Py_ssize_t i1 = 0 + cdef Py_ssize_t i2 = 0 + cdef Py_ssize_t i = 0 + + while i < array.shape[0]: + x = array[i] + if x < min1 : + min2 = min1 + i2 = i1 + min1 = x + i1 = i + elif x > min1 and x < min2 : + min2 = x + i2 = i + i += 1 + + return i2 diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index d9fcfdef..15f6ff34 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -4,7 +4,11 @@ except ImportError: import warnings warnings.warn('"cut_threshold" requires networkx') import numpy as np - +import _ncut +import _ncut_cy +from scipy.sparse import linalg +from scipy.sparse.linalg.eigen.arpack.arpack import ArpackNoConvergence as ANC +from scipy.sparse.linalg.eigen.arpack.arpack import ArpackError as APE def cut_threshold(labels, rag, thresh): """Combine regions seperated by weight less than threshold. @@ -60,3 +64,67 @@ def cut_threshold(labels, rag, thresh): map_array[label] = i return map_array[labels] + + +def cut_n(labels, rag, thresh): + + _ncut_relabel(rag,thresh) + + from_ = range(labels.max()+1) + to = [ rag.node[x]['ncut label'] for x in from_ ] + map_array = np.array(to) + + return map_array[labels] + +def _ncut_relabel(rag, cut_thresh = 0.0001): + d, w = _ncut.DW_matrix(rag) + error = False + + try: + m = w.shape[0] + vals,vectors = linalg.eigsh(d-w,M=d,which='SM',k = min(100,m-2)) + except ANC as e: + vals = e.eigenvalues + vectors = e.eigenvectors + if len(vals) == 0: + error = True + except ValueError: + error = True + except APE: + error = True + + if not error : + vals,vectors = np.real(vals), np.real(vectors) + index2 = _ncut_cy.argmin2(vals) + + ev = np.real(vectors[:,index2]) + ev = _ncut.norml(ev) + + mcut = np.inf + thresh = None + for t in np.arange(0,1,0.1): + mask = ev > t + cost = _ncut.ncut_cost(mask,d,w) + if cost < mcut : + mcut = cost + thresh = t + + if ( mcut < cut_thresh ): + mask = ev > thresh + + nodes1 = [ n for i,n in enumerate(rag.nodes()) if mask[i]] + nodes2 = [ n for i,n in enumerate(rag.nodes()) if not mask[i]] + + sub1 = rag.subgraph(nodes1) + sub2 = rag.subgraph(nodes2) + + _ncut_relabel(sub1,cut_thresh) + _ncut_relabel(sub2, cut_thresh) + return + + node = rag.nodes()[0] + new_label = rag.node[node]['labels'][0] + for n in rag.nodes(): + rag.node[n]['ncut label'] = new_label + + diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 7c1b7f07..4592ea36 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -11,6 +11,7 @@ except ImportError: import numpy as np from scipy.ndimage import filters from scipy import ndimage as nd +import math def min_weight(graph, src, dst, n): @@ -210,3 +211,51 @@ def rag_mean_color(image, labels, connectivity=2): graph[x][y]['weight'] = np.linalg.norm(diff) return graph + +def rag_similarity(image, labels, connectivity=2, sigma = 30.0): + graph = RAG() + + # The footprint is constructed in such a way that the first + # element in the array being passed to _add_edge_filter is + # the central value. + fp = nd.generate_binary_structure(labels.ndim, connectivity) + for d in range(fp.ndim): + fp = fp.swapaxes(0, d) + fp[0, ...] = 0 + fp = fp.swapaxes(0, d) + + + filters.generic_filter( + labels, + function=_add_edge_filter, + footprint=fp, + mode='nearest', + output=np.zeros(labels.shape, dtype=np.uint8), + extra_arguments=(graph,)) + + for n in graph: + graph.node[n].update({'labels': [n], + 'pixel count': 0, + 'total color': np.array([0, 0, 0], + dtype=np.double)}) + + for index in np.ndindex(labels.shape): + current = labels[index] + graph.node[current]['pixel count'] += 1 + graph.node[current]['total color'] += image[index] + + for n in graph: + graph.node[n]['mean color'] = (graph.node[n]['total color'] / + graph.node[n]['pixel count']) + + for x, y in graph.edges_iter(): + diff = graph.node[x]['mean color'] - graph.node[y]['mean color'] + diff = np.linalg.norm(diff) + + #if diff == 0: + # graph[x][y]['weight'] = 99999 + #else: + graph[x][y]['weight'] = math.e**(-(diff**2)/sigma) + #print graph[x][y]['weight'] + #print "diff",diff,"weight",math.e**(-(diff**2)/sigma) + return graph diff --git a/skimage/graph/setup.py b/skimage/graph/setup.py index 463d2739..edc7b653 100644 --- a/skimage/graph/setup.py +++ b/skimage/graph/setup.py @@ -17,6 +17,7 @@ def configuration(parent_package='', top_path=None): cython(['_spath.pyx'], working_path=base_path) cython(['_mcp.pyx'], working_path=base_path) cython(['heap.pyx'], working_path=base_path) + cython(['_ncut_cy.pyx'], working_path=base_path) config.add_extension('_spath', sources=['_spath.c'], include_dirs=[get_numpy_include_dirs()]) @@ -24,7 +25,8 @@ def configuration(parent_package='', top_path=None): include_dirs=[get_numpy_include_dirs()]) config.add_extension('heap', sources=['heap.c'], include_dirs=[get_numpy_include_dirs()]) - + config.add_extension('_ncut_cy', sources=['_ncut_cy.c'], + include_dirs=[get_numpy_include_dirs()]) return config if __name__ == '__main__': From ddf11bdfe85590a1319790ed97a1832bb3c789c2 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 23 Jul 2014 02:19:47 +0530 Subject: [PATCH 0197/1122] pep8 changes --- skimage/graph/graph_cut.py | 53 +++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 15f6ff34..f1971198 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -4,11 +4,12 @@ except ImportError: import warnings warnings.warn('"cut_threshold" requires networkx') import numpy as np -import _ncut -import _ncut_cy +from . import _ncut +from . import _ncut_cy from scipy.sparse import linalg -from scipy.sparse.linalg.eigen.arpack.arpack import ArpackNoConvergence as ANC -from scipy.sparse.linalg.eigen.arpack.arpack import ArpackError as APE +from scipy.sparse.linalg.eigen.arpack.arpack import ArpackNoConvergence +from scipy.sparse.linalg.eigen.arpack.arpack import ArpackError + def cut_threshold(labels, rag, thresh): """Combine regions seperated by weight less than threshold. @@ -68,57 +69,59 @@ def cut_threshold(labels, rag, thresh): def cut_n(labels, rag, thresh): - _ncut_relabel(rag,thresh) - - from_ = range(labels.max()+1) - to = [ rag.node[x]['ncut label'] for x in from_ ] + _ncut_relabel(rag, thresh) + + from_ = range(labels.max() + 1) + to = [rag.node[x]['ncut label'] for x in from_] map_array = np.array(to) - + return map_array[labels] -def _ncut_relabel(rag, cut_thresh = 0.0001): + +def _ncut_relabel(rag, cut_thresh=0.0001): d, w = _ncut.DW_matrix(rag) error = False try: m = w.shape[0] - vals,vectors = linalg.eigsh(d-w,M=d,which='SM',k = min(100,m-2)) - except ANC as e: + vals, vectors = linalg.eigsh(d - w, M=d, which='SM', + k=min(100, m - 2)) + except ArpackNoConvergence as e: vals = e.eigenvalues vectors = e.eigenvectors if len(vals) == 0: error = True except ValueError: error = True - except APE: + except ArpackError: error = True - - if not error : - vals,vectors = np.real(vals), np.real(vectors) + + if not error: + vals, vectors = np.real(vals), np.real(vectors) index2 = _ncut_cy.argmin2(vals) - ev = np.real(vectors[:,index2]) + ev = np.real(vectors[:, index2]) ev = _ncut.norml(ev) mcut = np.inf thresh = None - for t in np.arange(0,1,0.1): + for t in np.arange(0, 1, 0.1): mask = ev > t - cost = _ncut.ncut_cost(mask,d,w) - if cost < mcut : + cost = _ncut.ncut_cost(mask, d, w) + if cost < mcut: mcut = cost thresh = t - if ( mcut < cut_thresh ): + if (mcut < cut_thresh): mask = ev > thresh - nodes1 = [ n for i,n in enumerate(rag.nodes()) if mask[i]] - nodes2 = [ n for i,n in enumerate(rag.nodes()) if not mask[i]] + nodes1 = [n for i, n in enumerate(rag.nodes()) if mask[i]] + nodes2 = [n for i, n in enumerate(rag.nodes()) if not mask[i]] sub1 = rag.subgraph(nodes1) sub2 = rag.subgraph(nodes2) - _ncut_relabel(sub1,cut_thresh) + _ncut_relabel(sub1, cut_thresh) _ncut_relabel(sub2, cut_thresh) return @@ -126,5 +129,3 @@ def _ncut_relabel(rag, cut_thresh = 0.0001): new_label = rag.node[node]['labels'][0] for n in rag.nodes(): rag.node[n]['ncut label'] = new_label - - From 0971c11db6a662fd6be77e6843a51a700087337e Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 23 Jul 2014 02:27:33 +0530 Subject: [PATCH 0198/1122] Cleaned up rag.py --- skimage/graph/rag.py | 47 ++++---------------------------------------- 1 file changed, 4 insertions(+), 43 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 4592ea36..f4db630d 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -211,51 +211,12 @@ def rag_mean_color(image, labels, connectivity=2): graph[x][y]['weight'] = np.linalg.norm(diff) return graph - -def rag_similarity(image, labels, connectivity=2, sigma = 30.0): - graph = RAG() - - # The footprint is constructed in such a way that the first - # element in the array being passed to _add_edge_filter is - # the central value. - fp = nd.generate_binary_structure(labels.ndim, connectivity) - for d in range(fp.ndim): - fp = fp.swapaxes(0, d) - fp[0, ...] = 0 - fp = fp.swapaxes(0, d) - filters.generic_filter( - labels, - function=_add_edge_filter, - footprint=fp, - mode='nearest', - output=np.zeros(labels.shape, dtype=np.uint8), - extra_arguments=(graph,)) +def rag_similarity(image, labels, connectivity=2, sigma=30.0): + graph = rag_mean_color(image, labels, connectivity) - for n in graph: - graph.node[n].update({'labels': [n], - 'pixel count': 0, - 'total color': np.array([0, 0, 0], - dtype=np.double)}) + for x, w, d in graph.edges_iter(data=True): + d['weight'] = math.e ** (-(d['weight'] ** 2) / sigma) - for index in np.ndindex(labels.shape): - current = labels[index] - graph.node[current]['pixel count'] += 1 - graph.node[current]['total color'] += image[index] - - for n in graph: - graph.node[n]['mean color'] = (graph.node[n]['total color'] / - graph.node[n]['pixel count']) - - for x, y in graph.edges_iter(): - diff = graph.node[x]['mean color'] - graph.node[y]['mean color'] - diff = np.linalg.norm(diff) - - #if diff == 0: - # graph[x][y]['weight'] = 99999 - #else: - graph[x][y]['weight'] = math.e**(-(diff**2)/sigma) - #print graph[x][y]['weight'] - #print "diff",diff,"weight",math.e**(-(diff**2)/sigma) return graph From 997d4a3a682e4998539ecbfbfac197293b82914b Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 23 Jul 2014 02:37:15 +0530 Subject: [PATCH 0199/1122] cleaned up _ncut.py --- skimage/graph/_ncut.py | 36 ++++++++++++++---------------------- skimage/graph/_ncut_cy.pyx | 7 ++++--- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/skimage/graph/_ncut.py b/skimage/graph/_ncut.py index 9b321bd8..e31b9431 100644 --- a/skimage/graph/_ncut.py +++ b/skimage/graph/_ncut.py @@ -2,37 +2,29 @@ import networkx as nx import numpy as np from scipy import sparse + def DW_matrix(graph): - #print graph[4][4]['weight'] + W = nx.to_scipy_sparse_matrix(graph, format='csc') entries = W.sum(0) - D = sparse.dia_matrix( (entries,0),shape = W.shape).tocsc() - #print W[4,4] - - m,n = W.shape - #for i in range(n): - # W[i,i] = 1.0 - return D,W + D = sparse.dia_matrix((entries, 0), shape=W.shape).tocsc() + return D, W -def ncut_cost(mask,D,W): + +def ncut_cost(mask, D, W): mask = np.array(mask) - mask_list = [ np.logical_xor(mask[i], mask) for i in range(mask.shape[0])] + mask_list = [np.logical_xor(mask[i], mask) for i in range(mask.shape[0])] mask_array = np.array(mask_list) - - cut = float(W[mask_array].sum()/2.0) - #print W.todense() - #print mask_array.astype(int) - #print "cut=",cut - + + cut = float(W[mask_array].sum() / 2.0) assoc_a = D.data[mask].sum() assoc_b = D.data[np.logical_not(mask)].sum() - - #print cut - #print assoc_a,assoc_b - return (cut/assoc_a) + (cut/assoc_b) -def norml(a): + return (cut / assoc_a) + (cut / assoc_b) + + +def normalize(a): mi = a.min() mx = a.max() - return (a-mi)/(mx-mi) + return (a - mi) / (mx - mi) diff --git a/skimage/graph/_ncut_cy.pyx b/skimage/graph/_ncut_cy.pyx index fc3c0f3b..77cb18ea 100644 --- a/skimage/graph/_ncut_cy.pyx +++ b/skimage/graph/_ncut_cy.pyx @@ -5,6 +5,7 @@ cimport numpy as cnp import numpy as np + def argmin2(cnp.float64_t[:] array): cdef cnp.float64_t min1 = np.inf cdef cnp.float64_t min2 = np.inf @@ -14,14 +15,14 @@ def argmin2(cnp.float64_t[:] array): while i < array.shape[0]: x = array[i] - if x < min1 : + if x < min1: min2 = min1 i2 = i1 min1 = x i1 = i - elif x > min1 and x < min2 : + elif x > min1 and x < min2: min2 = x i2 = i i += 1 - + return i2 From 68b087ca48ee2fbd9a1f0ef2e303ff44f03a7528 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 23 Jul 2014 03:19:06 +0530 Subject: [PATCH 0200/1122] Docstrings --- skimage/graph/graph_cut.py | 2 +- skimage/graph/rag.py | 43 +++++++++++++++++++++++++------------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index f1971198..e49d1182 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -101,7 +101,7 @@ def _ncut_relabel(rag, cut_thresh=0.0001): index2 = _ncut_cy.argmin2(vals) ev = np.real(vectors[:, index2]) - ev = _ncut.norml(ev) + ev = _ncut.normalize(ev) mcut = np.inf thresh = None diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index f4db630d..0acede62 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -119,14 +119,15 @@ def _add_edge_filter(values, graph): return 0 -def rag_mean_color(image, labels, connectivity=2): +def rag_mean_color(image, labels, connectivity=2, mode='dissimilarity', + sigma=30.0): """Compute the Region Adjacency Graph using mean colors. Given an image and its initial segmentation, this method constructs the corresponsing Region Adjacency Graph (RAG). Each node in the RAG represents a set of pixels within `image` with the same label in `labels`. - The weight between two adjacent regions is the difference in their mean - color. + The weight between two adjacent regions represents how similar or + dissimilar two regions are depending on the `mode` parameter. Parameters ---------- @@ -141,6 +142,23 @@ def rag_mean_color(image, labels, connectivity=2): are considered adjacent. It can range from 1 to `labels.ndim`. Its behavior is the same as `connectivity` parameter in `scipy.ndimage.filters.generate_binary_structure`. + mode : str['similarity' | 'dissimilarity'] + The strategy to assign edge weights. + + 'similarity' : The weight between two adjacent regions is the + :math:`|c_1 - c_2|`, where :math:`c1` and :math:`c2` are the mean + colors of the two regions. It represents how different two regions + are. + + 'dissimilarity' : The weight between two adjacent is + :math:`e^{-d^2/sigma}` where :math:`d=|c_1 - c_2|`, where + :math:`c1` and :math:`c2` are the mean colors of the two regions. + It represents how similar two regions are. + sigma : float, optional + Used for computation when `mode='dissimilarity'`. It governs how close + to each other, two colors should be, for their corresponding edge + weight to be significant. A very large value of `sigma` could make + any two colors behave as though they were similar. Returns ------- @@ -206,17 +224,14 @@ def rag_mean_color(image, labels, connectivity=2): graph.node[n]['mean color'] = (graph.node[n]['total color'] / graph.node[n]['pixel count']) - for x, y in graph.edges_iter(): + for x, y, d in graph.edges_iter(data=True): diff = graph.node[x]['mean color'] - graph.node[y]['mean color'] - graph[x][y]['weight'] = np.linalg.norm(diff) - - return graph - - -def rag_similarity(image, labels, connectivity=2, sigma=30.0): - graph = rag_mean_color(image, labels, connectivity) - - for x, w, d in graph.edges_iter(data=True): - d['weight'] = math.e ** (-(d['weight'] ** 2) / sigma) + diff = np.linalg.norm(diff) + if mode == 'similarity': + d['weight'] = math.e ** (-(diff ** 2) / sigma) + elif mode == 'dissimilarity': + d['weight'] = diff + else: + raise ValueError("The mode '%s' is not recodnized" % mode) return graph From b05646e2015f7b0d67eefc073534e2fcf80bb195 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 23 Jul 2014 03:46:45 +0530 Subject: [PATCH 0201/1122] Docstring of graph_cut.py --- skimage/graph/graph_cut.py | 63 +++++++++++++++++++++++++++++++++++--- skimage/graph/rag.py | 2 +- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index e49d1182..3ae7b499 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -67,8 +67,44 @@ def cut_threshold(labels, rag, thresh): return map_array[labels] -def cut_n(labels, rag, thresh): +def cut_n(labels, rag, thresh=0.0001): + """Perform Normalized Graph cut on the Region Adjacency Graph. + Given an image's labels and its similarity RAG, recursively perform + a 2-wat Normalized cut on it. All nodes belonging to a subgraph + which cannot be cut further, are assigned a unique label in the + output. + + Parameters + ---------- + labels : ndarray + The array of labels. + rag : RAG + The region adjacency graph. + thresh : float + The threshold. A subgraph won't be further subdivided if the + value of the N-cut exceeds `thresh`. + + Returns + ------- + out : ndarray + The new labelled array. + + Examples + -------- + >>> from skimage import data, graph, segmentation, color, io + >>> img = data.lena() + >>> labels = segmentation.slic(img, compactness=30, n_segments=400) + >>> rag = graph.rag_mean_color(img, labels, mode='similarity') + >>> new_labels = graph.cut_n(labels, rag) + + References + ---------- + .. [1] Shi, J.; Malik, J., "Normalized cuts and image segmentation", + Pattern Analysis and Machine Intelligence, + IEEE Transactions on , vol.22, no.8, pp.888,905, Aug 2000 + + """ _ncut_relabel(rag, thresh) from_ = range(labels.max() + 1) @@ -78,7 +114,24 @@ def cut_n(labels, rag, thresh): return map_array[labels] -def _ncut_relabel(rag, cut_thresh=0.0001): +def _ncut_relabel(rag, thresh): + """Perform Normalized Graph cut on the Region Adjacency Graph. + + Recursively partition the graph into 2, untill further subdividing + yields a cut greather than `thresh` or such a cut cannot be computed. + For such a subgraph, assign a 'ncut label` attribute to all its nodes, + which is a their new unique label. + + Parameters + ---------- + labels : ndarray + The array of labels. + rag : RAG + The region adjacency graph. + thresh : float + The threshold. A subgraph won't be further subdivided if the + value of the N-cut exceeds `thresh`. + """ d, w = _ncut.DW_matrix(rag) error = False @@ -112,7 +165,7 @@ def _ncut_relabel(rag, cut_thresh=0.0001): mcut = cost thresh = t - if (mcut < cut_thresh): + if (mcut < thresh): mask = ev > thresh nodes1 = [n for i, n in enumerate(rag.nodes()) if mask[i]] @@ -121,8 +174,8 @@ def _ncut_relabel(rag, cut_thresh=0.0001): sub1 = rag.subgraph(nodes1) sub2 = rag.subgraph(nodes2) - _ncut_relabel(sub1, cut_thresh) - _ncut_relabel(sub2, cut_thresh) + _ncut_relabel(sub1, thresh) + _ncut_relabel(sub2, thresh) return node = rag.nodes()[0] diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 0acede62..65dff3ba 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -120,7 +120,7 @@ def _add_edge_filter(values, graph): def rag_mean_color(image, labels, connectivity=2, mode='dissimilarity', - sigma=30.0): + sigma=255.0): """Compute the Region Adjacency Graph using mean colors. Given an image and its initial segmentation, this method constructs the From 1e39dfc7a1ae8e5942b9916749b44ce7af4fed33 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 23 Jul 2014 03:54:57 +0530 Subject: [PATCH 0202/1122] rectified threshold mistake --- skimage/graph/graph_cut.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 3ae7b499..9f4f4789 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -157,16 +157,16 @@ def _ncut_relabel(rag, thresh): ev = _ncut.normalize(ev) mcut = np.inf - thresh = None + threshold = None for t in np.arange(0, 1, 0.1): mask = ev > t cost = _ncut.ncut_cost(mask, d, w) if cost < mcut: mcut = cost - thresh = t + threshold = t if (mcut < thresh): - mask = ev > thresh + mask = ev > threshold nodes1 = [n for i, n in enumerate(rag.nodes()) if mask[i]] nodes2 = [n for i, n in enumerate(rag.nodes()) if not mask[i]] From 273b9d081e3195f0acb5f5ae7e427c4f984864e7 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 23 Jul 2014 04:08:51 +0530 Subject: [PATCH 0203/1122] docstrings to _ncut.py --- skimage/graph/_ncut.py | 44 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/skimage/graph/_ncut.py b/skimage/graph/_ncut.py index e31b9431..3e961ad4 100644 --- a/skimage/graph/_ncut.py +++ b/skimage/graph/_ncut.py @@ -4,7 +4,23 @@ from scipy import sparse def DW_matrix(graph): + """Returns the diagonal and weight matrix of a graph. + Parameters + ---------- + graph : RAG + A Region Adjacency Graph. + + Returns + ------- + D : csc_matrix + The diagonal matrix of the graph. `D[i,i]` is the sum of weights of all + edges incident on `i`. All other enteries are `0`. + W : csc_matrix + The weight matrix of the graph. `W[i,j]` is the weight of the edge + joining `i` to `j`. + """ + #Cause sparse.eigsh prefers CSC format W = nx.to_scipy_sparse_matrix(graph, format='csc') entries = W.sum(0) D = sparse.dia_matrix((entries, 0), shape=W.shape).tocsc() @@ -12,7 +28,23 @@ def DW_matrix(graph): def ncut_cost(mask, D, W): + """Returns the N-cut cost of a bi-partition of a graph. + Parameters + ---------- + mask : ndarray + The mask for the nodes in the graph. Nodes corrsesponding to a `True` + value are in one set. + D : csc_matrix + The diagonal matrix of the graph. + W : csc_matrix + The weight matrix of the graph. + + Returns + ------- + cost : float + The cost of performing the N-cut. + """ mask = np.array(mask) mask_list = [np.logical_xor(mask[i], mask) for i in range(mask.shape[0])] mask_array = np.array(mask_list) @@ -25,6 +57,18 @@ def ncut_cost(mask, D, W): def normalize(a): + """Normalize values in an array between `0` and `1`. + + Parameters + ---------- + a : ndarray + The array to be normalized. + + Returns + ------- + out : ndarray + The normalized array. + """ mi = a.min() mx = a.max() return (a - mi) / (mx - mi) From 9c8b1efd2630a5e63c95bca614e00c0cc929d646 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 23 Jul 2014 04:12:23 +0530 Subject: [PATCH 0204/1122] docstring to _ncut_cy.pys --- skimage/graph/_ncut_cy.pyx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/skimage/graph/_ncut_cy.pyx b/skimage/graph/_ncut_cy.pyx index 77cb18ea..d8156ba4 100644 --- a/skimage/graph/_ncut_cy.pyx +++ b/skimage/graph/_ncut_cy.pyx @@ -7,6 +7,18 @@ import numpy as np def argmin2(cnp.float64_t[:] array): + """Return the index of the 2nd smallest value in an array. + + Parameters + ---------- + a : array + The array to process. + + Returns + ------- + i : int + The index of the second smallest value. + """ cdef cnp.float64_t min1 = np.inf cdef cnp.float64_t min2 = np.inf cdef Py_ssize_t i1 = 0 From 2017e350ff630106069adbfdc7f66a5f31f32086 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 23 Jul 2014 04:35:54 +0530 Subject: [PATCH 0205/1122] Comments to graph_cut.py --- skimage/graph/graph_cut.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 9f4f4789..5b7ff974 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -67,7 +67,7 @@ def cut_threshold(labels, rag, thresh): return map_array[labels] -def cut_n(labels, rag, thresh=0.0001): +def cut_n(labels, rag, thresh=0.0001, num_cuts=10): """Perform Normalized Graph cut on the Region Adjacency Graph. Given an image's labels and its similarity RAG, recursively perform @@ -84,6 +84,8 @@ def cut_n(labels, rag, thresh=0.0001): thresh : float The threshold. A subgraph won't be further subdivided if the value of the N-cut exceeds `thresh`. + num_cuts : int + The number or N-cuts to perform before determining the optimal one. Returns ------- @@ -105,7 +107,7 @@ def cut_n(labels, rag, thresh=0.0001): IEEE Transactions on , vol.22, no.8, pp.888,905, Aug 2000 """ - _ncut_relabel(rag, thresh) + _ncut_relabel(rag, thresh, num_cuts) from_ = range(labels.max() + 1) to = [rag.node[x]['ncut label'] for x in from_] @@ -114,7 +116,7 @@ def cut_n(labels, rag, thresh=0.0001): return map_array[labels] -def _ncut_relabel(rag, thresh): +def _ncut_relabel(rag, thresh, num_cuts): """Perform Normalized Graph cut on the Region Adjacency Graph. Recursively partition the graph into 2, untill further subdividing @@ -131,6 +133,8 @@ def _ncut_relabel(rag, thresh): thresh : float The threshold. A subgraph won't be further subdivided if the value of the N-cut exceeds `thresh`. + num_cuts : int + The number or N-cuts to perform before determining the optimal one. """ d, w = _ncut.DW_matrix(rag) error = False @@ -140,13 +144,17 @@ def _ncut_relabel(rag, thresh): vals, vectors = linalg.eigsh(d - w, M=d, which='SM', k=min(100, m - 2)) except ArpackNoConvergence as e: + # Not all eigenvectors converged, salvage the remaining. vals = e.eigenvalues vectors = e.eigenvectors if len(vals) == 0: + # No eigenvector converged. error = True except ValueError: + # k is too less, happens when the graph is of size 1 error = True except ArpackError: + # Arpack failing when two eigenvectors are same error = True if not error: @@ -158,7 +166,8 @@ def _ncut_relabel(rag, thresh): mcut = np.inf threshold = None - for t in np.arange(0, 1, 0.1): + # Perform evenly spaced n-cuts and determine the optimal one. + for t in np.linspace(0, 1, num_cuts, endpoint=False): mask = ev > t cost = _ncut.ncut_cost(mask, d, w) if cost < mcut: @@ -171,13 +180,18 @@ def _ncut_relabel(rag, thresh): nodes1 = [n for i, n in enumerate(rag.nodes()) if mask[i]] nodes2 = [n for i, n in enumerate(rag.nodes()) if not mask[i]] + # Sub divide and perform N-cut again sub1 = rag.subgraph(nodes1) sub2 = rag.subgraph(nodes2) - _ncut_relabel(sub1, thresh) - _ncut_relabel(sub2, thresh) + _ncut_relabel(sub1, thresh, num_cuts) + _ncut_relabel(sub2, thresh, num_cuts) return + # Either an errornous condition occurred, or N-cut wasn't small enough. + # The remaining graph is a region. + # Assign `ncut label` by picking any label from the existing nodes, since + # `labels` are unique, 'ncut label' is also unique. node = rag.nodes()[0] new_label = rag.node[node]['labels'][0] for n in rag.nodes(): From 22b5f4addf800fe959906978f1d5086faa0ddfcb Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 23 Jul 2014 04:47:17 +0530 Subject: [PATCH 0206/1122] Added example for ncut --- doc/examples/plot_ncut.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 doc/examples/plot_ncut.py diff --git a/doc/examples/plot_ncut.py b/doc/examples/plot_ncut.py new file mode 100644 index 00000000..941ef014 --- /dev/null +++ b/doc/examples/plot_ncut.py @@ -0,0 +1,32 @@ +""" +============== +Normalized Cut +============== + +This example constructs a Region Adjacency Graph (RAG) and recursively performs +a Normalized Cut on it. + +References +---------- +.. [1] Shi, J.; Malik, J., "Normalized cuts and image segmentation", + Pattern Analysis and Machine Intelligence, + IEEE Transactions on , vol.22, no.8, pp.888,905, Aug 2000 +""" +from skimage import graph, data, io, segmentation, color +from matplotlib import pyplot as plt + + +img = data.coffee() + +labels1 = segmentation.slic(img, compactness=30, n_segments=400) +out1 = color.label2rgb(labels1, img, kind='avg') + +g = graph.rag_mean_color(img, labels1, mode='similarity') +labels2 = graph.cut_n(labels1, g) +out2 = color.label2rgb(labels2, img, kind='avg') + +plt.figure() +io.imshow(out1) +plt.figure() +io.imshow(out2) +io.show() From c915aef7591c809dc3c3540de72038e0e11a3f01 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 23 Jul 2014 05:08:52 +0530 Subject: [PATCH 0207/1122] Updated bento file --- bento.info | 3 +++ skimage/graph/graph_cut.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bento.info b/bento.info index 227e51e8..d0891272 100644 --- a/bento.info +++ b/bento.info @@ -154,6 +154,9 @@ Library: Extension: skimage.feature._hessian_det_appx Sources: skimage/exposure/_hessian_det_appx.pyx + Extension: skimage.graph._ncut_cy + Sources: + skimage/graph/_ncut_cy.pyx Executable: skivi Module: skimage.scripts.skivi diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 5b7ff974..acb7d481 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -67,7 +67,7 @@ def cut_threshold(labels, rag, thresh): return map_array[labels] -def cut_n(labels, rag, thresh=0.0001, num_cuts=10): +def cut_n(labels, rag, thresh=0.001, num_cuts=10): """Perform Normalized Graph cut on the Region Adjacency Graph. Given an image's labels and its similarity RAG, recursively perform From d398b7fd9648b9d21cae3adc860d6d68650a35e1 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 23 Jul 2014 23:35:31 +0530 Subject: [PATCH 0208/1122] used standard eignen solver --- skimage/graph/graph_cut.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index acb7d481..c15eb7c0 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -138,10 +138,17 @@ def _ncut_relabel(rag, thresh, num_cuts): """ d, w = _ncut.DW_matrix(rag) error = False + try: m = w.shape[0] - vals, vectors = linalg.eigsh(d - w, M=d, which='SM', + d2 = d.copy() + # Since d is diagonal, we can directly operate on it's data + # the inverse + d2.data = 1.0/d2.data + # the square root + d2.data = np.sqrt(d2.data) + vals, vectors = linalg.eigsh(d2*(d - w)*d2, which='SM', k=min(100, m - 2)) except ArpackNoConvergence as e: # Not all eigenvectors converged, salvage the remaining. From fc9e8c4670404746e1b7cddf587d51fb82b3f264 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 23 Jul 2014 23:41:02 +0530 Subject: [PATCH 0209/1122] doc string correction --- skimage/graph/graph_cut.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index c15eb7c0..e703a111 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -71,7 +71,7 @@ def cut_n(labels, rag, thresh=0.001, num_cuts=10): """Perform Normalized Graph cut on the Region Adjacency Graph. Given an image's labels and its similarity RAG, recursively perform - a 2-wat Normalized cut on it. All nodes belonging to a subgraph + a 2-way normalized cut on it. All nodes belonging to a subgraph which cannot be cut further, are assigned a unique label in the output. From 62f090977662585a6a9cabdcbcfb07c23140c399 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 23 Jul 2014 23:46:33 +0530 Subject: [PATCH 0210/1122] Remove unused except caluses --- skimage/graph/graph_cut.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index e703a111..77e92bbd 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -150,19 +150,10 @@ def _ncut_relabel(rag, thresh, num_cuts): d2.data = np.sqrt(d2.data) vals, vectors = linalg.eigsh(d2*(d - w)*d2, which='SM', k=min(100, m - 2)) - except ArpackNoConvergence as e: - # Not all eigenvectors converged, salvage the remaining. - vals = e.eigenvalues - vectors = e.eigenvectors - if len(vals) == 0: - # No eigenvector converged. - error = True except ValueError: # k is too less, happens when the graph is of size 1 error = True - except ArpackError: - # Arpack failing when two eigenvectors are same - error = True + if not error: vals, vectors = np.real(vals), np.real(vectors) From 6cc0cf4a88f62d28b83aacbbe7e2c76dd31aa76b Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 24 Jul 2014 00:00:09 +0530 Subject: [PATCH 0211/1122] use map_array instead of modifying graph --- skimage/graph/graph_cut.py | 29 +++++++++++++---------------- skimage/graph/rag.py | 2 +- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 77e92bbd..a5a54141 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -7,8 +7,6 @@ import numpy as np from . import _ncut from . import _ncut_cy from scipy.sparse import linalg -from scipy.sparse.linalg.eigen.arpack.arpack import ArpackNoConvergence -from scipy.sparse.linalg.eigen.arpack.arpack import ArpackError def cut_threshold(labels, rag, thresh): @@ -107,22 +105,19 @@ def cut_n(labels, rag, thresh=0.001, num_cuts=10): IEEE Transactions on , vol.22, no.8, pp.888,905, Aug 2000 """ - _ncut_relabel(rag, thresh, num_cuts) - - from_ = range(labels.max() + 1) - to = [rag.node[x]['ncut label'] for x in from_] - map_array = np.array(to) + map_array = np.arange(labels.max() + 1) + _ncut_relabel(rag, thresh, num_cuts, map_array) return map_array[labels] -def _ncut_relabel(rag, thresh, num_cuts): +def _ncut_relabel(rag, thresh, num_cuts, map_array): """Perform Normalized Graph cut on the Region Adjacency Graph. Recursively partition the graph into 2, untill further subdividing yields a cut greather than `thresh` or such a cut cannot be computed. - For such a subgraph, assign a 'ncut label` attribute to all its nodes, - which is a their new unique label. + For such a subgraph, indices to labels of all its nodes map to a single + unique value. Parameters ---------- @@ -135,10 +130,12 @@ def _ncut_relabel(rag, thresh, num_cuts): value of the N-cut exceeds `thresh`. num_cuts : int The number or N-cuts to perform before determining the optimal one. + map_array : array + The array which maps old labels to new ones. This is modified inside + the function. """ d, w = _ncut.DW_matrix(rag) error = False - try: m = w.shape[0] @@ -154,7 +151,6 @@ def _ncut_relabel(rag, thresh, num_cuts): # k is too less, happens when the graph is of size 1 error = True - if not error: vals, vectors = np.real(vals), np.real(vectors) index2 = _ncut_cy.argmin2(vals) @@ -182,8 +178,8 @@ def _ncut_relabel(rag, thresh, num_cuts): sub1 = rag.subgraph(nodes1) sub2 = rag.subgraph(nodes2) - _ncut_relabel(sub1, thresh, num_cuts) - _ncut_relabel(sub2, thresh, num_cuts) + _ncut_relabel(sub1, thresh, num_cuts, map_array) + _ncut_relabel(sub2, thresh, num_cuts, map_array) return # Either an errornous condition occurred, or N-cut wasn't small enough. @@ -192,5 +188,6 @@ def _ncut_relabel(rag, thresh, num_cuts): # `labels` are unique, 'ncut label' is also unique. node = rag.nodes()[0] new_label = rag.node[node]['labels'][0] - for n in rag.nodes(): - rag.node[n]['ncut label'] = new_label + for n, d in rag.nodes_iter(data=True): + for l in d['labels']: + map_array[l] = new_label diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 65dff3ba..209ee602 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -156,7 +156,7 @@ def rag_mean_color(image, labels, connectivity=2, mode='dissimilarity', It represents how similar two regions are. sigma : float, optional Used for computation when `mode='dissimilarity'`. It governs how close - to each other, two colors should be, for their corresponding edge + to each other two colors should be, for their corresponding edge weight to be significant. A very large value of `sigma` could make any two colors behave as though they were similar. From f261e51faa0ff75d70832eb635481363e2a71aac Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 24 Jul 2014 00:01:56 +0530 Subject: [PATCH 0212/1122] rename method to cut_normalized --- doc/examples/plot_ncut.py | 2 +- skimage/graph/graph_cut.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/examples/plot_ncut.py b/doc/examples/plot_ncut.py index 941ef014..3160f710 100644 --- a/doc/examples/plot_ncut.py +++ b/doc/examples/plot_ncut.py @@ -22,7 +22,7 @@ labels1 = segmentation.slic(img, compactness=30, n_segments=400) out1 = color.label2rgb(labels1, img, kind='avg') g = graph.rag_mean_color(img, labels1, mode='similarity') -labels2 = graph.cut_n(labels1, g) +labels2 = graph.cut_normalized(labels1, g) out2 = color.label2rgb(labels2, img, kind='avg') plt.figure() diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index a5a54141..d02dde3a 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -65,7 +65,7 @@ def cut_threshold(labels, rag, thresh): return map_array[labels] -def cut_n(labels, rag, thresh=0.001, num_cuts=10): +def cut_normalized(labels, rag, thresh=0.001, num_cuts=10): """Perform Normalized Graph cut on the Region Adjacency Graph. Given an image's labels and its similarity RAG, recursively perform @@ -96,7 +96,7 @@ def cut_n(labels, rag, thresh=0.001, num_cuts=10): >>> img = data.lena() >>> labels = segmentation.slic(img, compactness=30, n_segments=400) >>> rag = graph.rag_mean_color(img, labels, mode='similarity') - >>> new_labels = graph.cut_n(labels, rag) + >>> new_labels = graph.cut_normalized(labels, rag) References ---------- From 7e2f33cb11b4de9e87e7e5ca55f46330b20865a1 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 24 Jul 2014 00:11:11 +0530 Subject: [PATCH 0213/1122] Refer sections of paper in comments --- skimage/graph/__init__.py | 4 ++-- skimage/graph/graph_cut.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/skimage/graph/__init__.py b/skimage/graph/__init__.py index 9fe00d3c..2d774b44 100644 --- a/skimage/graph/__init__.py +++ b/skimage/graph/__init__.py @@ -1,7 +1,7 @@ from .spath import shortest_path from .mcp import MCP, MCP_Geometric, MCP_Connect, MCP_Flexible, route_through_array from .rag import rag_mean_color, RAG -from .graph_cut import cut_threshold, cut_n +from .graph_cut import cut_threshold, cut_normalized __all__ = ['shortest_path', 'MCP', @@ -11,5 +11,5 @@ __all__ = ['shortest_path', 'route_through_array', 'rag_mean_color', 'cut_threshold', - 'cut_n', + 'cut_normalized', 'RAG'] diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index d02dde3a..1010877f 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -145,6 +145,7 @@ def _ncut_relabel(rag, thresh, num_cuts, map_array): d2.data = 1.0/d2.data # the square root d2.data = np.sqrt(d2.data) + # Refer to Equation 7 vals, vectors = linalg.eigsh(d2*(d - w)*d2, which='SM', k=min(100, m - 2)) except ValueError: @@ -152,6 +153,7 @@ def _ncut_relabel(rag, thresh, num_cuts, map_array): error = True if not error: + # Refer Section 3.2.3 vals, vectors = np.real(vals), np.real(vectors) index2 = _ncut_cy.argmin2(vals) @@ -160,6 +162,7 @@ def _ncut_relabel(rag, thresh, num_cuts, map_array): mcut = np.inf threshold = None + # Refer Section 3.1.3 # Perform evenly spaced n-cuts and determine the optimal one. for t in np.linspace(0, 1, num_cuts, endpoint=False): mask = ev > t @@ -178,6 +181,7 @@ def _ncut_relabel(rag, thresh, num_cuts, map_array): sub1 = rag.subgraph(nodes1) sub2 = rag.subgraph(nodes2) + # Refer Section 3.2.5 _ncut_relabel(sub1, thresh, num_cuts, map_array) _ncut_relabel(sub2, thresh, num_cuts, map_array) return From d8c0b2e7dd87672e112af49c6e85d45edb8c52a1 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 24 Jul 2014 00:58:33 +0530 Subject: [PATCH 0214/1122] Add unit test --- skimage/graph/tests/test_rag.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 753bcb74..8bdefee8 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -2,6 +2,8 @@ import numpy as np from skimage import graph from skimage._shared.version_requirements import is_installed from numpy.testing.decorators import skipif +from skimage import segmentation +from numpy import testing def max_edge(g, src, dst, n): @@ -66,3 +68,31 @@ def test_threshold_cut(): # Two labels assert new_labels.max() == 1 + + +def test_cut_normalized(): + + img = np.zeros((100, 100, 3), dtype='uint8') + img[:50, :50] = 255, 255, 255 + img[:50, 50:] = 254, 254, 254 + img[50:, :50] = 2, 2, 2 + img[50:, 50:] = 1, 1, 1 + + labels = np.zeros((100, 100), dtype='uint8') + labels[:50, :50] = 0 + labels[:50, 50:] = 1 + labels[50:, :50] = 2 + labels[50:, 50:] = 3 + + rag = graph.rag_mean_color(img, labels, mode='similarity') + new_labels = graph.cut_normalized(labels, rag) + new_labels, _, _ = segmentation.relabel_sequential(new_labels) + + # Two labels + assert new_labels.max() == 1 + + +def test_rag_error(): + img = np.zeros((10, 10, 3), dtype='uint8') + labels = np.zeros((10, 10), dtype='uint8') + testing.assert_raises(ValueError, graph.rag_mean_color,img, labels, 2, 'non existant mode') From 07cb79cd27838c2a9fbbe0e951209ea067eb9181 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 31 Jul 2014 23:05:47 +0530 Subject: [PATCH 0215/1122] Cut cost is computed by Cython code --- skimage/graph/_ncut.py | 18 +++++++++--------- skimage/graph/_ncut_cy.pyx | 27 +++++++++++++++++++++++++++ skimage/graph/graph_cut.py | 2 +- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/skimage/graph/_ncut.py b/skimage/graph/_ncut.py index 3e961ad4..4874b252 100644 --- a/skimage/graph/_ncut.py +++ b/skimage/graph/_ncut.py @@ -1,10 +1,11 @@ import networkx as nx import numpy as np from scipy import sparse +from . import _ncut_cy -def DW_matrix(graph): - """Returns the diagonal and weight matrix of a graph. +def DW_matrices(graph): + """Returns the diagonal and weight matrices of a graph. Parameters ---------- @@ -14,15 +15,15 @@ def DW_matrix(graph): Returns ------- D : csc_matrix - The diagonal matrix of the graph. `D[i,i]` is the sum of weights of all - edges incident on `i`. All other enteries are `0`. + The diagonal matrix of the graph. `D[i, i]` is the sum of weights of + all edges incident on `i`. All other enteries are `0`. W : csc_matrix - The weight matrix of the graph. `W[i,j]` is the weight of the edge + The weight matrix of the graph. `W[i, j]` is the weight of the edge joining `i` to `j`. """ #Cause sparse.eigsh prefers CSC format W = nx.to_scipy_sparse_matrix(graph, format='csc') - entries = W.sum(0) + entries = W.sum(axis=0) D = sparse.dia_matrix((entries, 0), shape=W.shape).tocsc() return D, W @@ -46,10 +47,9 @@ def ncut_cost(mask, D, W): The cost of performing the N-cut. """ mask = np.array(mask) - mask_list = [np.logical_xor(mask[i], mask) for i in range(mask.shape[0])] - mask_array = np.array(mask_list) + cut = _ncut_cy.cut_cost(mask, W) - cut = float(W[mask_array].sum() / 2.0) + # Cause D has elements only along diagonal assoc_a = D.data[mask].sum() assoc_b = D.data[np.logical_not(mask)].sum() diff --git a/skimage/graph/_ncut_cy.pyx b/skimage/graph/_ncut_cy.pyx index d8156ba4..8c416cbc 100644 --- a/skimage/graph/_ncut_cy.pyx +++ b/skimage/graph/_ncut_cy.pyx @@ -38,3 +38,30 @@ def argmin2(cnp.float64_t[:] array): i += 1 return i2 + + +def cut_cost(mask, W): + mask = np.array(mask) + + cdef Py_ssize_t num_rows, num_cols + cdef cnp.int32_t row, col + cdef cnp.int32_t[:] indices = W.indices + cdef cnp.int32_t[:] indptr = W.indptr + cdef cnp.float64_t[:] data = W.data + cdef cnp.int32_t row_index + cdef cnp.double_t cost = 0 + + num_rows = W.shape[0] + num_cols = W.shape[1] + + col = 0 + while col < num_cols: + row_index = indptr[col] + while row_index < indptr[col+1]: + row = indices[row_index] + if mask[row] != mask[col]: + cost += data[row_index] + row_index += 1 + col += 1 + + return cost*0.5 diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 1010877f..af5048fd 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -134,7 +134,7 @@ def _ncut_relabel(rag, thresh, num_cuts, map_array): The array which maps old labels to new ones. This is modified inside the function. """ - d, w = _ncut.DW_matrix(rag) + d, w = _ncut.DW_matrices(rag) error = False try: From 452921d9f239423aee9e3aae35be9ecc6e40ab1e Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 6 Aug 2014 02:44:33 +0530 Subject: [PATCH 0216/1122] API changes, comments and docstrings --- skimage/graph/__init__.py | 2 + skimage/graph/_ncut.py | 27 +++++---- skimage/graph/_ncut_cy.pyx | 49 +++++++++++------ skimage/graph/graph_cut.py | 110 +++++++++++++++++++++++++------------ 4 files changed, 126 insertions(+), 62 deletions(-) diff --git a/skimage/graph/__init__.py b/skimage/graph/__init__.py index 2d774b44..aca73bc4 100644 --- a/skimage/graph/__init__.py +++ b/skimage/graph/__init__.py @@ -2,6 +2,7 @@ from .spath import shortest_path from .mcp import MCP, MCP_Geometric, MCP_Connect, MCP_Flexible, route_through_array from .rag import rag_mean_color, RAG from .graph_cut import cut_threshold, cut_normalized +ncut = cut_normalized __all__ = ['shortest_path', 'MCP', @@ -12,4 +13,5 @@ __all__ = ['shortest_path', 'rag_mean_color', 'cut_threshold', 'cut_normalized', + 'ncut', 'RAG'] diff --git a/skimage/graph/_ncut.py b/skimage/graph/_ncut.py index 4874b252..fbefca27 100644 --- a/skimage/graph/_ncut.py +++ b/skimage/graph/_ncut.py @@ -21,20 +21,20 @@ def DW_matrices(graph): The weight matrix of the graph. `W[i, j]` is the weight of the edge joining `i` to `j`. """ - #Cause sparse.eigsh prefers CSC format + # sparse.eighsh is most efficient with CSC-formatted input W = nx.to_scipy_sparse_matrix(graph, format='csc') entries = W.sum(axis=0) D = sparse.dia_matrix((entries, 0), shape=W.shape).tocsc() return D, W -def ncut_cost(mask, D, W): +def ncut_cost(cut, D, W): """Returns the N-cut cost of a bi-partition of a graph. Parameters ---------- - mask : ndarray - The mask for the nodes in the graph. Nodes corrsesponding to a `True` + cut : ndarray + The mask for the nodes in the graph. Nodes corressponding to a `True` value are in one set. D : csc_matrix The diagonal matrix of the graph. @@ -45,15 +45,22 @@ def ncut_cost(mask, D, W): ------- cost : float The cost of performing the N-cut. + + References + ---------- + .. [1] Normalized Cuts and Image Segmentation, Jianbo Shi and + Jitendra Malik, IEEE Transactions on Pattern Analysis and Machine + Intelligence, Page 889, Equation 2. """ - mask = np.array(mask) - cut = _ncut_cy.cut_cost(mask, W) + cut = np.array(cut) + cut_cost = _ncut_cy.cut_cost(cut, W) - # Cause D has elements only along diagonal - assoc_a = D.data[mask].sum() - assoc_b = D.data[np.logical_not(mask)].sum() + # D has elements only along the diagonal, one per node, so we can directly + # index the data attribute with cut. + assoc_a = D.data[cut].sum() + assoc_b = D.data[np.logical_not(cut)].sum() - return (cut / assoc_a) + (cut / assoc_b) + return (cut_cost / assoc_a) + (cut_cost / assoc_b) def normalize(a): diff --git a/skimage/graph/_ncut_cy.pyx b/skimage/graph/_ncut_cy.pyx index 8c416cbc..0f859edf 100644 --- a/skimage/graph/_ncut_cy.pyx +++ b/skimage/graph/_ncut_cy.pyx @@ -16,52 +16,65 @@ def argmin2(cnp.float64_t[:] array): Returns ------- - i : int + min_idx2 : int The index of the second smallest value. """ cdef cnp.float64_t min1 = np.inf cdef cnp.float64_t min2 = np.inf - cdef Py_ssize_t i1 = 0 - cdef Py_ssize_t i2 = 0 + cdef Py_ssize_t min_idx1 = 0 + cdef Py_ssize_t min_idx2 = 0 cdef Py_ssize_t i = 0 + cdef Py_ssize_t n = array.shape[0] - while i < array.shape[0]: + for i in range(n): x = array[i] if x < min1: min2 = min1 - i2 = i1 + min_idx2 = min_idx1 min1 = x - i1 = i + min_idx1 = i elif x > min1 and x < min2: min2 = x - i2 = i + min_idx2 = i i += 1 - return i2 + return min_idx2 -def cut_cost(mask, W): - mask = np.array(mask) +def cut_cost(cut, W): + """Return the total weight of crossing edges in a bi-partition. + Parameters + ---------- + cut : array + A array of booleans. Elements set to `True` belong to one + set. + W : array + The weight matrix of the graph. + + Returns + ------- + cost : float + The total weight of crossing edges. + """ + cdef cnp.ndarray[cnp.uint8_t, cast = True] cut_mask = np.array(cut) cdef Py_ssize_t num_rows, num_cols cdef cnp.int32_t row, col cdef cnp.int32_t[:] indices = W.indices cdef cnp.int32_t[:] indptr = W.indptr - cdef cnp.float64_t[:] data = W.data + cdef cnp.double_t[:] data = W.data.astype(np.double) cdef cnp.int32_t row_index cdef cnp.double_t cost = 0 num_rows = W.shape[0] num_cols = W.shape[1] - col = 0 - while col < num_cols: - row_index = indptr[col] - while row_index < indptr[col+1]: + for col in range(num_cols): + for row_index in range(indptr[col], indptr[col + 1]): row = indices[row_index] - if mask[row] != mask[col]: - cost += data[row_index] + if cut_mask[row] != cut_mask[col]: + cost += data[row_index] row_index += 1 col += 1 - return cost*0.5 + return cost * 0.5 diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index af5048fd..9c133bd3 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -111,6 +111,65 @@ def cut_normalized(labels, rag, thresh=0.001, num_cuts=10): return map_array[labels] +def partition_by_cut(cut, rag): + """Compute resulting subgraphs from given bi-parition. + + Parameters + ---------- + cut : array + A array of booleans. Elements set to `True` belong to one + set. + rag : RAG + The Region Adjacency Graph. + + Returns + ------- + sub1, sub2 : RAG + The two resulting subgraphs from the bi-partition. + """ + nodes1 = [n for i, n in enumerate(rag.nodes()) if cut[i]] + nodes2 = [n for i, n in enumerate(rag.nodes()) if not cut[i]] + + sub1 = rag.subgraph(nodes1) + sub2 = rag.subgraph(nodes2) + + return sub1, sub2 + + +def get_min_ncut(ev, d, w, num_cuts): + """Threshold an eigen vector evenly, to determine minimum ncut. + + Parameters + ---------- + ev : array + The eigenvector to threshold. + d : ndarray + The diagonal matrix of the graph. + w : ndarray + The weight matrix of the graph. + num_cuts : int + The number of evenly spaced thresholds to check for. + + Returns + ------- + threshold, mcut : float + The threshold which produced the minimum ncut, and the value of the + ncut itself. + """ + mcut = np.inf + + # Refer Shi & Malik 2001, Section 3.1.3, Page 892 + # Perform evenly spaced n-cuts and determine the optimal one. + for t in np.linspace(0, 1, num_cuts, endpoint=False): + mask = ev > t + cost = _ncut.ncut_cost(mask, d, w) + if cost < mcut: + mcut = cost + threshold = t + + return threshold, mcut + + def _ncut_relabel(rag, thresh, num_cuts, map_array): """Perform Normalized Graph cut on the Region Adjacency Graph. @@ -135,58 +194,41 @@ def _ncut_relabel(rag, thresh, num_cuts, map_array): the function. """ d, w = _ncut.DW_matrices(rag) - error = False + stop = False + m = w.shape[0] - try: - m = w.shape[0] + if m > 2: d2 = d.copy() # Since d is diagonal, we can directly operate on it's data # the inverse - d2.data = 1.0/d2.data + d2.data = 1.0 / d2.data # the square root d2.data = np.sqrt(d2.data) - # Refer to Equation 7 - vals, vectors = linalg.eigsh(d2*(d - w)*d2, which='SM', + # Refer Shi & Malik 2001, Equation 7, Page 891 + vals, vectors = linalg.eigsh(d2 * (d - w) * d2, which='SM', k=min(100, m - 2)) - except ValueError: - # k is too less, happens when the graph is of size 1 - error = True + else: + stop = True - if not error: - # Refer Section 3.2.3 + if not stop: + # Pick second smalles eigen vector. + # Refer Shi & Malik 2001, Section 3.2.3, Page 893 vals, vectors = np.real(vals), np.real(vectors) index2 = _ncut_cy.argmin2(vals) + ev = _ncut.normalize(vectors[:, index2]) - ev = np.real(vectors[:, index2]) - ev = _ncut.normalize(ev) - - mcut = np.inf - threshold = None - # Refer Section 3.1.3 - # Perform evenly spaced n-cuts and determine the optimal one. - for t in np.linspace(0, 1, num_cuts, endpoint=False): - mask = ev > t - cost = _ncut.ncut_cost(mask, d, w) - if cost < mcut: - mcut = cost - threshold = t - + threshold, mcut = get_min_ncut(ev, d, w, num_cuts) if (mcut < thresh): - mask = ev > threshold - - nodes1 = [n for i, n in enumerate(rag.nodes()) if mask[i]] - nodes2 = [n for i, n in enumerate(rag.nodes()) if not mask[i]] - + cut_mask = ev > threshold # Sub divide and perform N-cut again - sub1 = rag.subgraph(nodes1) - sub2 = rag.subgraph(nodes2) + # Refer Shi & Malik 2001, Section 3.2.5, Page 893 + sub1, sub2 = partition_by_cut(cut_mask, rag) - # Refer Section 3.2.5 _ncut_relabel(sub1, thresh, num_cuts, map_array) _ncut_relabel(sub2, thresh, num_cuts, map_array) return - # Either an errornous condition occurred, or N-cut wasn't small enough. + # The N-cut wasn't small enough, or could not be computed. # The remaining graph is a region. # Assign `ncut label` by picking any label from the existing nodes, since # `labels` are unique, 'ncut label' is also unique. From 69fec94326e56ec009f62c7626422be71fbe3f08 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 6 Aug 2014 02:46:29 +0530 Subject: [PATCH 0217/1122] rag copy in threshold cut --- skimage/graph/graph_cut.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 9c133bd3..bc694409 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -47,6 +47,7 @@ def cut_threshold(labels, rag, thresh): """ # Because deleting edges while iterating through them produces an error. + rag = rag.copy() to_remove = [(x, y) for x, y, d in rag.edges_iter(data=True) if d['weight'] >= thresh] rag.remove_edges_from(to_remove) From 3ad2b682d6c96dff7ab232cee092710bd0adb2aa Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 6 Aug 2014 02:47:24 +0530 Subject: [PATCH 0218/1122] test skip --- skimage/graph/tests/test_rag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 8bdefee8..4cd5fd36 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -69,7 +69,7 @@ def test_threshold_cut(): # Two labels assert new_labels.max() == 1 - +@skipif(not is_installed('networkx')) def test_cut_normalized(): img = np.zeros((100, 100, 3), dtype='uint8') From 35c9746b7407f495875dc8df116442eb287c58dc Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 6 Aug 2014 02:48:03 +0530 Subject: [PATCH 0219/1122] comment --- skimage/graph/graph_cut.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index bc694409..65d14585 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -232,7 +232,7 @@ def _ncut_relabel(rag, thresh, num_cuts, map_array): # The N-cut wasn't small enough, or could not be computed. # The remaining graph is a region. # Assign `ncut label` by picking any label from the existing nodes, since - # `labels` are unique, 'ncut label' is also unique. + # `labels` are unique, `new_label` is also unique. node = rag.nodes()[0] new_label = rag.node[node]['labels'][0] for n, d in rag.nodes_iter(data=True): From 9371c4f0e738763238ede8e99788f19770b8cb92 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 6 Aug 2014 22:38:18 +0530 Subject: [PATCH 0220/1122] Fixed potential import error --- skimage/graph/_ncut.py | 6 +++++- skimage/graph/graph_cut.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/skimage/graph/_ncut.py b/skimage/graph/_ncut.py index fbefca27..46ffb528 100644 --- a/skimage/graph/_ncut.py +++ b/skimage/graph/_ncut.py @@ -1,4 +1,8 @@ -import networkx as nx +try: + import networkx as nx +except ImportError: + import warnings + warnings.warn('RAGs require networkx') import numpy as np from scipy import sparse from . import _ncut_cy diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 65d14585..e44f8a5c 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -2,7 +2,7 @@ try: import networkx as nx except ImportError: import warnings - warnings.warn('"cut_threshold" requires networkx') + warnings.warn('RAGs require networkx') import numpy as np from . import _ncut from . import _ncut_cy From 1fa7bced8d10d08a82ed039b9959179e1ca19a88 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 6 Aug 2014 23:45:35 +0530 Subject: [PATCH 0221/1122] skip error test --- skimage/graph/tests/test_rag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 4cd5fd36..81889d6f 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -91,7 +91,7 @@ def test_cut_normalized(): # Two labels assert new_labels.max() == 1 - +@skipif(not is_installed('networkx')) def test_rag_error(): img = np.zeros((10, 10, 3), dtype='uint8') labels = np.zeros((10, 10), dtype='uint8') From 6d9a3c3913ff6411929c062c3939f6f0493f3ace Mon Sep 17 00:00:00 2001 From: Adam Feuer Date: Wed, 6 Aug 2014 21:14:00 -0700 Subject: [PATCH 0222/1122] reformat comments to be standard format - clarified function names - comments in standard format - removed extra blank line - as per code review feedback --- skimage/io/_plugins/pil_plugin.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 9510f4d0..9e7bb499 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -19,13 +19,25 @@ from six import string_types def imread(fname, dtype=None): """Load an image from file. + Parameters + ---------- + fname : file name as string + dtype : numpy dtype object or string specifier + Specifies data type of array elements. + + """ im = Image.open(fname) return pil_to_ndarray(im, dtype) def pil_to_ndarray(im, dtype=None): - """import an image from a PIL Image object in memory + """Import a PIL Image object to an ndarray, in memory. + + Parameters + ---------- + Refer to ``imread``. + """ fp = im.fp if hasattr(im, 'fp') else None @@ -74,11 +86,11 @@ def _palette_is_grayscale(pil_image): def ndarray_to_pil(arr, format_str=None): - """Export an image as PIL object + """Export an ndarray to a PIL object. Parameters ---------- - See imsave + Refer to ``imsave``. """ arr = np.asarray(arr).squeeze() @@ -115,7 +127,6 @@ def ndarray_to_pil(arr, format_str=None): return img - def imsave(fname, arr, format_str=None): """Save an image to disk. From 0ff30f6f4cdc34e86d46b29be1e7d0dbc78b152f Mon Sep 17 00:00:00 2001 From: Adam Feuer Date: Wed, 6 Aug 2014 21:16:21 -0700 Subject: [PATCH 0223/1122] added Adam Feuer (myself) to contributors file - as per code review feedback from @stefanv --- CONTRIBUTORS.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 140842ab..30f432f4 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -182,3 +182,6 @@ - Axel Donath Blob Detection + +- Adam Feuer + PIL Image import and export improvements From 95f18c71da002d5979da2290f37cc380bd5811c9 Mon Sep 17 00:00:00 2001 From: capitanbatata Date: Fri, 8 Aug 2014 10:43:57 +0200 Subject: [PATCH 0224/1122] Added support for different illuminants. --- skimage/__init__.py | 10 +-- skimage/color/colorconv.py | 163 +++++++++++++++++++++++++++++++------ 2 files changed, 144 insertions(+), 29 deletions(-) diff --git a/skimage/__init__.py b/skimage/__init__.py index 59c80ed2..aacde5d6 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -65,11 +65,11 @@ from skimage._shared.utils import deprecated as _deprecated pkg_dir = _osp.abspath(_osp.dirname(__file__)) data_dir = _osp.join(pkg_dir, 'data') -try: - from .version import version as __version__ -except ImportError: - __version__ = "unbuilt-dev" -del version +# try: +# from .version import version as __version__ +# except ImportError: +# __version__ = "unbuilt-dev" +# del version try: diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 80f115ee..03fd8413 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -322,8 +322,86 @@ gray_from_rgb = np.array([[0.2125, 0.7154, 0.0721], [0, 0, 0]]) # CIE LAB constants for Observer= 2A, Illuminant= D65 +## NOTE: this is actually the XYZ values for the illuminant above. lab_ref_white = np.array([0.95047, 1., 1.08883]) +## XYZ coordinates of the illuminants, scaled to [0, 1]. For each illuminant I we have: +## +## illuminant[I][0] corresponds to the XYZ coordinates for the 2 degree +## field of view. +## +## illuminant[I][1] corresponds to the XYZ coordinates for the 10 degree +## field of view. +## +## The XYZ coordinates are calculated from [1], using the formula: +## +## X = x * ( Y / y ) +## Y = Y +## Z = ( 1 - x - y ) * ( Y / y ) +## +## where Y = 1. The only exception is the illuminant "D65" with aperture angle +## 2, whose coordinates are copied from 'lab_ref_white' for +## backward-compatibility reasons. +## +## References +## ---------- +## .. [1] http://en.wikipedia.org/wiki/Standard_illuminant + +illuminants = \ + {"A": [[1.098466069456375, 1, 0.3558228003436005], \ + [1.111420406956693, 1, 0.3519978321919493]], \ + "D50": [[0.9642119944211994, 1, 0.8251882845188288], \ + [0.9672062750333777, 1, 0.8142801513128616]], \ + "D55": [[0.956797052643698, 1, 0.9214805860173273], \ + [0.9579665682254781, 1, 0.9092525159847462]], \ + "D65": [lab_ref_white, \ + [0.94809667673716, 1, 1.0730513595166162]], \ + "D75": [[0.9497220898840717, 1, 1.226393520724154], \ + [0.9441713925645873, 1, 1.2064272211720228]], \ + "E": [[1.0, 1.0, 1.0], \ + [1.0, 1.0, 1.0]] + } + +def get_xyz_coords(illuminant, observer): + """Get the XYZ coordinates of the given illuminant and observer [1]. Currently + supported illuminants are: "A", "D50", "D55", "D65", "D75", "E". + + Parameters + ---------- + illuminant: string + The name of the illuminant (the function is NOT case sensitive). + observer: int + The aperture angle of the observer. + + Returns + ------- + xyz_coords: list + A list with 3 elements containing the XYZ coordinates of the given + illuminant. + + Raises + ------ + ValueError + If either the illuminant or the observer angle are not supported or + unknown. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Standard_illuminant + + """ + illuminant = illuminant.upper() + if illuminant in illuminants.keys(): + if observer == 2: + idx = 0 + elif observer == 10: + idx = 10 + else: + ValueError("Unknown observer \"{}\"".format(observer)) + return illuminants[illuminant][idx] + else: + ValueError("Unknown illuminant \"{}\"".format(illuminant)) + # Haematoxylin-Eosin-DAB colorspace # From original Ruifrok's paper: A. C. Ruifrok and D. A. Johnston, @@ -666,9 +744,8 @@ def gray2rgb(image): return np.concatenate(3 * (image,), axis=-1) else: raise ValueError("Input image expected to be RGB, RGBA or gray.") - - -def xyz2lab(xyz): + +def xyz2lab(xyz, illuminant = "D65", observer = 2999999999): """XYZ to CIE-LAB color space conversion. Parameters @@ -676,6 +753,10 @@ def xyz2lab(xyz): xyz : array_like The image in XYZ format, in a 3- or 4-D array of shape ``(.., ..,[ ..,] 3)``. + illuminant: string + The name of the illuminant (the function is NOT case sensitive). + observer: int + The aperture angle of the observer. Returns ------- @@ -687,11 +768,15 @@ def xyz2lab(xyz): ------ ValueError If `xyz` is not a 3-D array of shape ``(.., ..,[ ..,] 3)``. + ValueError + If either the illuminant or the observer angle are not supported or + unknown. Notes ----- - Observer= 2A, Illuminant= D65 - CIE XYZ tristimulus values x_ref = 95.047, y_ref = 100., z_ref = 108.883 + By default Observer= 2A, Illuminant= D65. CIE XYZ tristimulus values x_ref + = 95.047, y_ref = 100., z_ref = 108.883. See function 'get_xyz_coords' for + a list of supported illuminants. References ---------- @@ -705,11 +790,14 @@ def xyz2lab(xyz): >>> lena = data.lena() >>> lena_xyz = rgb2xyz(lena) >>> lena_lab = xyz2lab(lena_xyz) + """ arr = _prepare_colorarray(xyz) + xyz_ref_white = get_xyz_coords(illuminant, observer) + # scale by CIE XYZ tristimulus values of the reference white point - arr = arr / lab_ref_white + arr = arr / xyz_ref_white # Nonlinear distortion and linear transformation mask = arr > 0.008856 @@ -725,14 +813,17 @@ def xyz2lab(xyz): return np.concatenate([x[..., np.newaxis] for x in [L, a, b]], axis=-1) - -def lab2xyz(lab): +def lab2xyz(lab, illuminant = "D65", observer = 2): """CIE-LAB to XYZcolor space conversion. Parameters ---------- lab : array_like The image in lab format, in a 3-D array of shape ``(.., .., 3)``. + illuminant: string + The name of the illuminant (the function is NOT case sensitive). + observer: int + The aperture angle of the observer. Returns ------- @@ -743,11 +834,16 @@ def lab2xyz(lab): ------ ValueError If `lab` is not a 3-D array of shape ``(.., .., 3)``. + ValueError + If either the illuminant or the observer angle are not supported or + unknown. + Notes ----- - Observer = 2A, Illuminant = D65 - CIE XYZ tristimulus values x_ref = 95.047, y_ref = 100., z_ref = 108.883 + By default Observer= 2A, Illuminant= D65. CIE XYZ tristimulus values x_ref + = 95.047, y_ref = 100., z_ref = 108.883. See function 'get_xyz_coords' for + a list of supported illuminants. References ---------- @@ -769,11 +865,11 @@ def lab2xyz(lab): out[mask] = np.power(out[mask], 3.) out[~mask] = (out[~mask] - 16.0 / 116.) / 7.787 - # rescale Observer= 2 deg, Illuminant= D65 - out *= lab_ref_white + # rescale to the reference white (illuminant) + xyz_ref_white = get_xyz_coords(illuminant, observer) + out *= xyz_ref_white return out - def rgb2lab(rgb): """RGB to lab color space conversion. @@ -826,7 +922,7 @@ def lab2rgb(lab): return xyz2rgb(lab2xyz(lab)) -def xyz2luv(xyz): +def xyz2luv(xyz, illuminant = "D65", observer = 2): """XYZ to CIE-Luv color space conversion. Parameters @@ -834,6 +930,10 @@ def xyz2luv(xyz): xyz : (M, N, [P,] 3) array_like The 3 or 4 dimensional image in XYZ format. Final dimension denotes channels. + illuminant: string + The name of the illuminant (the function is NOT case sensitive). + observer: int + The aperture angle of the observer. Returns ------- @@ -844,11 +944,16 @@ def xyz2luv(xyz): ------ ValueError If `xyz` is not a 3-D or 4-D array of shape ``(M, N, [P,] 3)``. + ValueError + If either the illuminant or the observer angle are not supported or + unknown. Notes ----- - XYZ conversion weights use Observer = 2A. Reference whitepoint for D65 - Illuminant, with XYZ tristimulus values of ``(95.047, 100., 108.883)``. + By default XYZ conversion weights use Observer = 2A. Reference whitepoint + for D65 Illuminant, with XYZ tristimulus values of ``(95.047, 100., + 108.883)``. See function 'get_xyz_coords' for a list of supported + illuminants. References ---------- @@ -871,13 +976,14 @@ def xyz2luv(xyz): eps = np.finfo(np.float).eps # compute y_r and L - L = y / lab_ref_white[1] + xyz_ref_white = get_xyz_coords(illuminant, observer) + L = y / xyz_ref_white[1] mask = L > 0.008856 L[mask] = 116. * np.power(L[mask], 1. / 3.) - 16. L[~mask] = 903.3 * L[~mask] - u0 = 4*lab_ref_white[0] / np.dot([1, 15, 3], lab_ref_white) - v0 = 9*lab_ref_white[1] / np.dot([1, 15, 3], lab_ref_white) + u0 = 4*xyz_ref_white[0] / np.dot([1, 15, 3], xyz_ref_white) + v0 = 9*xyz_ref_white[1] / np.dot([1, 15, 3], xyz_ref_white) # u' and v' helper functions def fu(X, Y, Z): @@ -893,7 +999,7 @@ def xyz2luv(xyz): return np.concatenate([q[..., np.newaxis] for q in [L, u, v]], axis=-1) -def luv2xyz(luv): +def luv2xyz(luv, illuminant = "D65", observer = 2): """CIE-Luv to XYZ color space conversion. Parameters @@ -901,6 +1007,10 @@ def luv2xyz(luv): luv : (M, N, [P,] 3) array_like The 3 or 4 dimensional image in CIE-Luv format. Final dimension denotes channels. + illuminant: string + The name of the illuminant (the function is NOT case sensitive). + observer: int + The aperture angle of the observer. Returns ------- @@ -911,11 +1021,15 @@ def luv2xyz(luv): ------ ValueError If `luv` is not a 3-D or 4-D array of shape ``(M, N, [P,] 3)``. + ValueError + If either the illuminant or the observer angle are not supported or + unknown. Notes ----- XYZ conversion weights use Observer = 2A. Reference whitepoint for D65 - Illuminant, with XYZ tristimulus values of ``(95.047, 100., 108.883)``. + Illuminant, with XYZ tristimulus values of ``(95.047, 100., 108.883)``. See + function 'get_xyz_coords' for a list of supported illuminants. References ---------- @@ -935,12 +1049,13 @@ def luv2xyz(luv): mask = y > 7.999625 y[mask] = np.power((y[mask]+16.) / 116., 3.) y[~mask] = y[~mask] / 903.3 - y *= lab_ref_white[1] + xyz_ref_white = get_xyz_coords(illuminant, observer) + y *= xyz_ref_white[1] # reference white x,z uv_weights = [1, 15, 3] - u0 = 4*lab_ref_white[0] / np.dot(uv_weights, lab_ref_white) - v0 = 9*lab_ref_white[1] / np.dot(uv_weights, lab_ref_white) + u0 = 4*xyz_ref_white[0] / np.dot(uv_weights, xyz_ref_white) + v0 = 9*xyz_ref_white[1] / np.dot(uv_weights, xyz_ref_white) # compute intermediate values a = u0 + u / (13.*L + eps) From ba664df275007b34392becded4e4a5b73a8a752a Mon Sep 17 00:00:00 2001 From: capitanbatata Date: Fri, 8 Aug 2014 13:28:45 +0200 Subject: [PATCH 0225/1122] Added new tests. The support for a wider range of illuminants is now tested. --- skimage/color/tests/data/lab_array_a_2.npy | Bin 0 -> 200 bytes skimage/color/tests/data/lab_array_d50_10.npy | Bin 0 -> 200 bytes skimage/color/tests/data/lab_array_d50_2.npy | Bin 0 -> 200 bytes skimage/color/tests/data/lab_array_d55_10.npy | Bin 0 -> 200 bytes skimage/color/tests/data/lab_array_d55_2.npy | Bin 0 -> 200 bytes skimage/color/tests/data/lab_array_d65_10.npy | Bin 0 -> 200 bytes skimage/color/tests/data/lab_array_d65_2.npy | Bin 0 -> 200 bytes skimage/color/tests/data/lab_array_d75_10.npy | Bin 0 -> 200 bytes skimage/color/tests/data/lab_array_d75_2.npy | Bin 0 -> 200 bytes skimage/color/tests/data/lab_array_e_2.npy | Bin 0 -> 200 bytes skimage/color/tests/data/luv_array_a_2.npy | Bin 0 -> 200 bytes skimage/color/tests/data/luv_array_d50_10.npy | Bin 0 -> 200 bytes skimage/color/tests/data/luv_array_d50_2.npy | Bin 0 -> 200 bytes skimage/color/tests/data/luv_array_d55_10.npy | Bin 0 -> 200 bytes skimage/color/tests/data/luv_array_d55_2.npy | Bin 0 -> 200 bytes skimage/color/tests/data/luv_array_d65_10.npy | Bin 0 -> 200 bytes skimage/color/tests/data/luv_array_d65_2.npy | Bin 0 -> 200 bytes skimage/color/tests/data/luv_array_d75_10.npy | Bin 0 -> 200 bytes skimage/color/tests/data/luv_array_d75_2.npy | Bin 0 -> 200 bytes skimage/color/tests/data/luv_array_e_2.npy | Bin 0 -> 200 bytes 20 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 skimage/color/tests/data/lab_array_a_2.npy create mode 100644 skimage/color/tests/data/lab_array_d50_10.npy create mode 100644 skimage/color/tests/data/lab_array_d50_2.npy create mode 100644 skimage/color/tests/data/lab_array_d55_10.npy create mode 100644 skimage/color/tests/data/lab_array_d55_2.npy create mode 100644 skimage/color/tests/data/lab_array_d65_10.npy create mode 100644 skimage/color/tests/data/lab_array_d65_2.npy create mode 100644 skimage/color/tests/data/lab_array_d75_10.npy create mode 100644 skimage/color/tests/data/lab_array_d75_2.npy create mode 100644 skimage/color/tests/data/lab_array_e_2.npy create mode 100644 skimage/color/tests/data/luv_array_a_2.npy create mode 100644 skimage/color/tests/data/luv_array_d50_10.npy create mode 100644 skimage/color/tests/data/luv_array_d50_2.npy create mode 100644 skimage/color/tests/data/luv_array_d55_10.npy create mode 100644 skimage/color/tests/data/luv_array_d55_2.npy create mode 100644 skimage/color/tests/data/luv_array_d65_10.npy create mode 100644 skimage/color/tests/data/luv_array_d65_2.npy create mode 100644 skimage/color/tests/data/luv_array_d75_10.npy create mode 100644 skimage/color/tests/data/luv_array_d75_2.npy create mode 100644 skimage/color/tests/data/luv_array_e_2.npy diff --git a/skimage/color/tests/data/lab_array_a_2.npy b/skimage/color/tests/data/lab_array_a_2.npy new file mode 100644 index 0000000000000000000000000000000000000000..cd1b81d1573f07d8e84f9b2e0367cbda6ddd4541 GIT binary patch literal 200 zcmbR27wQ`j$;jZwP_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I%IItqq53dWi`3bhIlz}3XeEOdUZm&2Ug!{^T^`#Z>9ILy59imL+yUJ&WP z)_2|RcDMO~V+U;P=1mVfkjDB!W4D@vgN*{`QgT|K7eqSz z)?VUzS)KiW*u(w&QIRGG(pW!e>{fGdcx}Tz^;Kf9Lli^xBD>4+2P$?T|8csgB_wba=iT^>hHjS7eqR+ zZ~n4eT!8t2g+Ns2@_6F|X{;YKcB?r!Jip}k{E4^-@!cGgPP-QghX T1H*^0UIz|oK3um!+sy$0d#pU! literal 0 HcmV?d00001 diff --git a/skimage/color/tests/data/lab_array_d55_10.npy b/skimage/color/tests/data/lab_array_d55_10.npy new file mode 100644 index 0000000000000000000000000000000000000000..566bc1105ba4f214ccec74a210bb9880a1115202 GIT binary patch literal 200 zcmbR27wQ`j$;jZwP_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I%IItqq53dWi`3bhIlz}3XeEOdUZm&2qL&DPP!gB|*Rd|&kEpT7eGUJ&V^ z)aSjg^4*vHy2~uRl22$HNMrq=v0Kf-VWNW6OqV&q4wveVsCT@HJy5Y@+F2`EcZd3$ T89)7%ybioSE5@s;_UI#dSJhb2B=k5Ri+;BaC literal 0 HcmV?d00001 diff --git a/skimage/color/tests/data/lab_array_d65_10.npy b/skimage/color/tests/data/lab_array_d65_10.npy new file mode 100644 index 0000000000000000000000000000000000000000..4ac9c0ae704ae50a63e8d508ca2e0664cdfd154e GIT binary patch literal 200 zcmbR27wQ`j$;jZwP_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I%IItqq53dWi`3bhIlz}3XeEOdUZm%}2LIvXwN5QoZ`?w$jC0~{Fef=Gu6 zVfq)^e%-Q9U~vs-SpI&08tVs*-D(aFF?;)Cp4|?1xccb5qKa|!fr=f|&RWU3JLDz$ S=H?ypJRq1HyY<644+j93vOT*1 literal 0 HcmV?d00001 diff --git a/skimage/color/tests/data/lab_array_d65_2.npy b/skimage/color/tests/data/lab_array_d65_2.npy new file mode 100644 index 0000000000000000000000000000000000000000..71e233d1bbfe0dac3cdbaa262ae264b5492f5bf6 GIT binary patch literal 200 zcmbR27wQ`j$;jZwP_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I%IItqq53dWi`3bhIlz}3XeEOdUZmqW|D)VKfGLL5}?rOXsP7vR8v7eqQR zC^T;2=qa^ln4t6UQ9$edG}aFqyVV>V%wt2Or=1CQ=;*)G3nCpX zg!zBmzWdSsHS;}N&+AeSX{;YKcB?r!tk|-AO2yY;2bCvl7F)zb9;nzc?W~ooyTd2b TZ|%=!dmhL<(viF8rKbY`{{}z` literal 0 HcmV?d00001 diff --git a/skimage/color/tests/data/lab_array_d75_2.npy b/skimage/color/tests/data/lab_array_d75_2.npy new file mode 100644 index 0000000000000000000000000000000000000000..1ef368552e9c96e9cae3654857e8ee8557b6b9e0 GIT binary patch literal 200 zcmbR27wQ`j$;jZwP_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I%IItqq53dWi`3bhIlz}3XeEOdUZmqSwpKYKiHh(l9$QS3GUKnDiAAku;1 z_B{sWmV@?87SfKq0`d-NtRFOXt2sEF^+@x`y%_9JAN%jqD(T1r6+5P#wUTvrSZ*BP RV|K~&fLy6!T7!U>0{~7jHp>72 literal 0 HcmV?d00001 diff --git a/skimage/color/tests/data/lab_array_e_2.npy b/skimage/color/tests/data/lab_array_e_2.npy new file mode 100644 index 0000000000000000000000000000000000000000..f795d3be3bd25be7f0da67611a8d79c26d04983a GIT binary patch literal 200 zcmbR27wQ`j$;jZwP_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I%IItqq53dWi`3bhIlz}3XeEOdUZmqXs`_jhdyf*g*$G2h6W7vR8v7eqSf zxKCQV_PD|UHAk1<&-ld;q_KX`*sbQ^Q1voE`rz~+hX%%sHn;GY0~I@_owbs6cc@jE S>2Q0M_kq*5`PG`_Jsbe$b38r( literal 0 HcmV?d00001 diff --git a/skimage/color/tests/data/luv_array_a_2.npy b/skimage/color/tests/data/luv_array_a_2.npy new file mode 100644 index 0000000000000000000000000000000000000000..ae25c1194c05cba348092895621014365f3d6759 GIT binary patch literal 200 zcmbR27wQ`j$;jZwP_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I%IItqq53dWi`3bhIlz}3XeEOdUZm&3M2JG|0wB{*kWbqq_KX`*sbQ^p!iEI>*RBX1IAT`%?Z}Y2P$?G}aFqyVV>Ve#rV~nmy1skda!tH{@T!fr=f|&RWU3J8(@s Svt$Xs$AQ{OY626zJsbc%k3C}m literal 0 HcmV?d00001 diff --git a/skimage/color/tests/data/luv_array_d55_2.npy b/skimage/color/tests/data/luv_array_d55_2.npy new file mode 100644 index 0000000000000000000000000000000000000000..88daa5db37c8ed97e8ff335b700fde08a08ae9bc GIT binary patch literal 200 zcmbR27wQ`j$;jZwP_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I%IItqq53dWi`3bhIlz}3XeEOdUZm&5voe{Yxjr#e`#tLjM6wsl~@3nCr3 zgx)^qR#iMOedqCN4rPM_X{;YKcB?r!q&3dpx;#wdK-kGSQzSkm9H`ha?W~ooyMu3M T?&{oo?gz@>wcnUE(Zc}%)Tus* literal 0 HcmV?d00001 diff --git a/skimage/color/tests/data/luv_array_d65_10.npy b/skimage/color/tests/data/luv_array_d65_10.npy new file mode 100644 index 0000000000000000000000000000000000000000..43ce45abda121403c4d895528b7d954b6276a515 GIT binary patch literal 200 zcmbR27wQ`j$;jZwP_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I%IItqq53dWi`3bhIlz}3XeEOdUZm%}G%({m0FQyqL_uKiLiadKe53nCpB z+-P>uJ3e#&_6xQ=u5*9xPhzRN@=ZO+ literal 0 HcmV?d00001 diff --git a/skimage/color/tests/data/luv_array_d65_2.npy b/skimage/color/tests/data/luv_array_d65_2.npy new file mode 100644 index 0000000000000000000000000000000000000000..75ed4792d1f9c5554d16d33e232b2a6543247f5f GIT binary patch literal 200 zcmbR27wQ`j$;jZwP_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I%IItqq53dWi`3bhIlz}3XeEOdUZmqUdGL&c_tsSdZ2TYCf^I5{xj1(6O6 zZ@5yc^nL9QTw3O_zh&b7G}aFqyVV>VRL;ql?mnY*;1TCi-#ILfX%3-+T_LmF+#DG2f=CCo zXKg!PCW$#1`Tb>{*`n!?#`-~Hx0-{4#pP|w+v4O7Y%`qUa=tJAK*f$}XRT!29Zpz2 SyUQ}e<-k{-BR}%({T%=(H#~a) literal 0 HcmV?d00001 diff --git a/skimage/color/tests/data/luv_array_e_2.npy b/skimage/color/tests/data/luv_array_e_2.npy new file mode 100644 index 0000000000000000000000000000000000000000..f2f4c28044ef0231ba226495126dda9d52bbc27a GIT binary patch literal 200 zcmbR27wQ`j$;jZwP_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I%IItqq53dWi`3bhIlz}3XeEOdUZm&1}tDf17WNO4fB*RP9Yb97+93nCrX zY!MQ;R%>uzdgE5( Date: Fri, 8 Aug 2014 14:22:27 +0200 Subject: [PATCH 0226/1122] Added tests for the new functionality. --- skimage/color/colorconv.py | 9 +++-- skimage/color/tests/test_colorconv.py | 58 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 03fd8413..46268bd2 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -392,15 +392,16 @@ def get_xyz_coords(illuminant, observer): """ illuminant = illuminant.upper() if illuminant in illuminants.keys(): + idx = 100; if observer == 2: idx = 0 elif observer == 10: - idx = 10 + idx = 1 else: - ValueError("Unknown observer \"{}\"".format(observer)) + raise ValueError("Unknown observer \"{}\"".format(observer)) return illuminants[illuminant][idx] else: - ValueError("Unknown illuminant \"{}\"".format(illuminant)) + raise ValueError("Unknown illuminant \"{}\"".format(illuminant)) # Haematoxylin-Eosin-DAB colorspace @@ -745,7 +746,7 @@ def gray2rgb(image): else: raise ValueError("Input image expected to be RGB, RGBA or gray.") -def xyz2lab(xyz, illuminant = "D65", observer = 2999999999): +def xyz2lab(xyz, illuminant = "D65", observer = 2): """XYZ to CIE-LAB color space conversion. Parameters diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index 63336a76..faf9b9db 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -233,10 +233,39 @@ class TestColorconv(TestCase): assert_array_almost_equal(xyz2lab(self.xyz_array), self.lab_array, decimal=3) + ## Thest the conversion with the rest of the illuminants. + for I in ["d50", "d55", "d65", "d75"]: + for obs in [2, 10]: + print("testing illuminant={}, observer={}".format(I, obs)) + fname = "lab_array_{}_{}.npy".format(I, obs) + lab_array_I_obs = np.load(os.path.join('data', fname)) + assert_array_almost_equal(lab_array_I_obs, + xyz2lab(self.xyz_array, I, obs), decimal=2) + for I in ["a", "e"]: + print("testing illuminant={}, observer=2".format(I)) + fname = "lab_array_{}_2.npy".format(I) + lab_array_I_obs = np.load(os.path.join('data', fname)) + assert_array_almost_equal(lab_array_I_obs, + xyz2lab(self.xyz_array, I, 2), decimal=2) + def test_lab2xyz(self): assert_array_almost_equal(lab2xyz(self.lab_array), self.xyz_array, decimal=3) + ## Thest the conversion with the rest of the illuminants. + for I in ["d50", "d55", "d65", "d75"]: + for obs in [2, 10]: + fname = "lab_array_{}_{}.npy".format(I, obs) + lab_array_I_obs = np.load(os.path.join('data', fname)) + assert_array_almost_equal(lab2xyz(lab_array_I_obs, I, obs), + self.xyz_array, decimal=3) + for I in ["a", "e"]: + fname = "lab_array_{}_2.npy".format(I, obs) + lab_array_I_obs = np.load(os.path.join('data', fname)) + assert_array_almost_equal(lab2xyz(lab_array_I_obs, I, 2), + self.xyz_array, decimal=3) + + def test_rgb2lab_brucelindbloom(self): """ Test the RGB->Lab conversion by comparing to the calculator on the @@ -267,10 +296,39 @@ class TestColorconv(TestCase): assert_array_almost_equal(xyz2luv(self.xyz_array), self.luv_array, decimal=3) + ## Thest the conversion with the rest of the illuminants. + for I in ["d50", "d55", "d65", "d75"]: + for obs in [2, 10]: + print("testing illuminant={}, observer={}".format(I, obs)) + fname = "luv_array_{}_{}.npy".format(I, obs) + luv_array_I_obs = np.load(os.path.join('data', fname)) + assert_array_almost_equal(luv_array_I_obs, + xyz2luv(self.xyz_array, I, obs), decimal=2) + for I in ["a", "e"]: + print("testing illuminant={}, observer=2".format(I)) + fname = "luv_array_{}_2.npy".format(I) + luv_array_I_obs = np.load(os.path.join('data', fname)) + assert_array_almost_equal(luv_array_I_obs, + xyz2luv(self.xyz_array, I, 2), decimal=2) + + def test_luv2xyz(self): assert_array_almost_equal(luv2xyz(self.luv_array), self.xyz_array, decimal=3) + ## Thest the conversion with the rest of the illuminants. + for I in ["d50", "d55", "d65", "d75"]: + for obs in [2, 10]: + fname = "luv_array_{}_{}.npy".format(I, obs) + luv_array_I_obs = np.load(os.path.join('data', fname)) + assert_array_almost_equal(luv2xyz(luv_array_I_obs, I, obs), + self.xyz_array, decimal=3) + for I in ["a", "e"]: + fname = "luv_array_{}_2.npy".format(I, obs) + luv_array_I_obs = np.load(os.path.join('data', fname)) + assert_array_almost_equal(luv2xyz(luv_array_I_obs, I, 2), + self.xyz_array, decimal=3) + def test_rgb2luv_brucelindbloom(self): """ Test the RGB->Lab conversion by comparing to the calculator on the From 3b3bb01270d1253a3fc015cfeb04bd4fe31eed4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 8 Aug 2014 08:27:57 -0400 Subject: [PATCH 0227/1122] Fix denoise tests --- skimage/restoration/tests/test_denoise.py | 40 ++++++++++++----------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/skimage/restoration/tests/test_denoise.py b/skimage/restoration/tests/test_denoise.py index c1091009..fe4ce59a 100644 --- a/skimage/restoration/tests/test_denoise.py +++ b/skimage/restoration/tests/test_denoise.py @@ -7,13 +7,15 @@ from skimage import restoration, data, color, img_as_float np.random.seed(1234) -lena = img_as_float(data.lena()[:256, :256]) +lena = img_as_float(data.lena()[:128, :128]) lena_gray = color.rgb2gray(lena) +checkerboard_gray = img_as_float(data.checkerboard()) +checkerboard = color.gray2rgb(checkerboard_gray) def test_denoise_tv_chambolle_2d(): # lena image - img = lena_gray + img = lena_gray.copy() # add noise to lena img += 0.5 * img.std() * np.random.rand(*img.shape) # clip noise so that it does not exceed allowed range for float images. @@ -70,7 +72,7 @@ def test_denoise_tv_chambolle_3d(): def test_denoise_tv_bregman_2d(): - img = lena_gray + img = checkerboard_gray.copy() # add some random noise img += 0.5 * img.std() * np.random.rand(*img.shape) img = np.clip(img, 0, 1) @@ -78,14 +80,14 @@ def test_denoise_tv_bregman_2d(): out1 = restoration.denoise_tv_bregman(img, weight=10) out2 = restoration.denoise_tv_bregman(img, weight=5) - # make sure noise is reduced - assert img.std() > out1.std() - assert out1.std() > out2.std() + # make sure noise is reduced in the checkerboard cells + assert img[30:45, 5:15].std() > out1[30:45, 5:15].std() + assert out1[30:45, 5:15].std() > out2[30:45, 5:15].std() def test_denoise_tv_bregman_float_result_range(): # lena image - img = lena_gray + img = lena_gray.copy() int_lena = np.multiply(img, 255).astype(np.uint8) assert np.max(int_lena) > 1 denoised_int_lena = restoration.denoise_tv_bregman(int_lena, weight=60.0) @@ -96,7 +98,7 @@ def test_denoise_tv_bregman_float_result_range(): def test_denoise_tv_bregman_3d(): - img = lena + img = checkerboard.copy() # add some random noise img += 0.5 * img.std() * np.random.rand(*img.shape) img = np.clip(img, 0, 1) @@ -104,13 +106,13 @@ def test_denoise_tv_bregman_3d(): out1 = restoration.denoise_tv_bregman(img, weight=10) out2 = restoration.denoise_tv_bregman(img, weight=5) - # make sure noise is reduced - assert img.std() > out1.std() - assert out1.std() > out2.std() + # make sure noise is reduced in the checkerboard cells + assert img[30:45, 5:15].std() > out1[30:45, 5:15].std() + assert out1[30:45, 5:15].std() > out2[30:45, 5:15].std() def test_denoise_bilateral_2d(): - img = lena_gray + img = checkerboard_gray.copy() # add some random noise img += 0.5 * img.std() * np.random.rand(*img.shape) img = np.clip(img, 0, 1) @@ -120,13 +122,13 @@ def test_denoise_bilateral_2d(): out2 = restoration.denoise_bilateral(img, sigma_range=0.2, sigma_spatial=30) - # make sure noise is reduced - assert img.std() > out1.std() - assert out1.std() > out2.std() + # make sure noise is reduced in the checkerboard cells + assert img[30:45, 5:15].std() > out1[30:45, 5:15].std() + assert out1[30:45, 5:15].std() > out2[30:45, 5:15].std() def test_denoise_bilateral_3d(): - img = lena + img = checkerboard.copy() # add some random noise img += 0.5 * img.std() * np.random.rand(*img.shape) img = np.clip(img, 0, 1) @@ -136,9 +138,9 @@ def test_denoise_bilateral_3d(): out2 = restoration.denoise_bilateral(img, sigma_range=0.2, sigma_spatial=30) - # make sure noise is reduced - assert img.std() > out1.std() - assert out1.std() > out2.std() + # make sure noise is reduced in the checkerboard cells + assert img[30:45, 5:15].std() > out1[30:45, 5:15].std() + assert out1[30:45, 5:15].std() > out2[30:45, 5:15].std() if __name__ == "__main__": From 6d0bf723d700e943e977db27012821906a33bf86 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Fri, 8 Aug 2014 23:16:38 +0530 Subject: [PATCH 0228/1122] Docstring changes and typos --- skimage/graph/_ncut.py | 6 +++--- skimage/graph/_ncut_cy.pyx | 2 +- skimage/graph/graph_cut.py | 12 ++++++------ skimage/graph/rag.py | 8 ++++---- skimage/graph/tests/test_rag.py | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/skimage/graph/_ncut.py b/skimage/graph/_ncut.py index 46ffb528..2735bbd2 100644 --- a/skimage/graph/_ncut.py +++ b/skimage/graph/_ncut.py @@ -19,10 +19,10 @@ def DW_matrices(graph): Returns ------- D : csc_matrix - The diagonal matrix of the graph. `D[i, i]` is the sum of weights of + The diagonal matrix of the graph. ``D[i, i]`` is the sum of weights of all edges incident on `i`. All other enteries are `0`. W : csc_matrix - The weight matrix of the graph. `W[i, j]` is the weight of the edge + The weight matrix of the graph. ``W[i, j]`` is the weight of the edge joining `i` to `j`. """ # sparse.eighsh is most efficient with CSC-formatted input @@ -62,7 +62,7 @@ def ncut_cost(cut, D, W): # D has elements only along the diagonal, one per node, so we can directly # index the data attribute with cut. assoc_a = D.data[cut].sum() - assoc_b = D.data[np.logical_not(cut)].sum() + assoc_b = D.data[~cut].sum() return (cut_cost / assoc_a) + (cut_cost / assoc_b) diff --git a/skimage/graph/_ncut_cy.pyx b/skimage/graph/_ncut_cy.pyx index 0f859edf..0888be72 100644 --- a/skimage/graph/_ncut_cy.pyx +++ b/skimage/graph/_ncut_cy.pyx @@ -6,7 +6,7 @@ cimport numpy as cnp import numpy as np -def argmin2(cnp.float64_t[:] array): +def argmin2(cnp.double_t[:] array): """Return the index of the 2nd smallest value in an array. Parameters diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index e44f8a5c..b86f6c48 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -89,11 +89,11 @@ def cut_normalized(labels, rag, thresh=0.001, num_cuts=10): Returns ------- out : ndarray - The new labelled array. + The new labeled array. Examples -------- - >>> from skimage import data, graph, segmentation, color, io + >>> from skimage import data, graph, segmentation >>> img = data.lena() >>> labels = segmentation.slic(img, compactness=30, n_segments=400) >>> rag = graph.rag_mean_color(img, labels, mode='similarity') @@ -103,7 +103,7 @@ def cut_normalized(labels, rag, thresh=0.001, num_cuts=10): ---------- .. [1] Shi, J.; Malik, J., "Normalized cuts and image segmentation", Pattern Analysis and Machine Intelligence, - IEEE Transactions on , vol.22, no.8, pp.888,905, Aug 2000 + IEEE Transactions on , vol.22, no.8, pp.888,905, August 2000 """ map_array = np.arange(labels.max() + 1) @@ -138,7 +138,7 @@ def partition_by_cut(cut, rag): def get_min_ncut(ev, d, w, num_cuts): - """Threshold an eigen vector evenly, to determine minimum ncut. + """Threshold an eigenvector evenly, to determine minimum ncut. Parameters ---------- @@ -174,7 +174,7 @@ def get_min_ncut(ev, d, w, num_cuts): def _ncut_relabel(rag, thresh, num_cuts, map_array): """Perform Normalized Graph cut on the Region Adjacency Graph. - Recursively partition the graph into 2, untill further subdividing + Recursively partition the graph into 2, until further subdivision yields a cut greather than `thresh` or such a cut cannot be computed. For such a subgraph, indices to labels of all its nodes map to a single unique value. @@ -212,7 +212,7 @@ def _ncut_relabel(rag, thresh, num_cuts, map_array): stop = True if not stop: - # Pick second smalles eigen vector. + # Pick second smalles eigenvector. # Refer Shi & Malik 2001, Section 3.2.3, Page 893 vals, vectors = np.real(vals), np.real(vectors) index2 = _ncut_cy.argmin2(vals) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 209ee602..f33936c2 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -146,17 +146,17 @@ def rag_mean_color(image, labels, connectivity=2, mode='dissimilarity', The strategy to assign edge weights. 'similarity' : The weight between two adjacent regions is the - :math:`|c_1 - c_2|`, where :math:`c1` and :math:`c2` are the mean + :math:`|c_1 - c_2|`, where :math:`c_1` and :math:`c_2` are the mean colors of the two regions. It represents how different two regions are. 'dissimilarity' : The weight between two adjacent is :math:`e^{-d^2/sigma}` where :math:`d=|c_1 - c_2|`, where - :math:`c1` and :math:`c2` are the mean colors of the two regions. + :math:`c_1` and :math:`c_2` are the mean colors of the two regions. It represents how similar two regions are. sigma : float, optional - Used for computation when `mode='dissimilarity'`. It governs how close - to each other two colors should be, for their corresponding edge + Used for computation when `mode` is "dissimilarity". It governs how + close to each other two colors should be, for their corresponding edge weight to be significant. A very large value of `sigma` could make any two colors behave as though they were similar. diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 81889d6f..1e643fc4 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -95,4 +95,4 @@ def test_cut_normalized(): def test_rag_error(): img = np.zeros((10, 10, 3), dtype='uint8') labels = np.zeros((10, 10), dtype='uint8') - testing.assert_raises(ValueError, graph.rag_mean_color,img, labels, 2, 'non existant mode') + testing.assert_raises(ValueError, graph.rag_mean_color, img, labels, 2, 'non existant mode') From 31cf1acf24e5ed141bb45137dbe9a3aa75595831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 2 Aug 2014 02:12:15 -0400 Subject: [PATCH 0229/1122] Fix isodata-thresholding according to Zachary Pincus --- skimage/filter/thresholding.py | 87 +++++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 27 deletions(-) diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index da74fe47..8a80ddb0 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -193,10 +193,19 @@ def threshold_yen(image, nbins=256): return bin_centers[crit.argmax()] -def threshold_isodata(image, nbins=256): - """Return threshold value based on ISODATA method. +def isodata(image, nbins=256, return_all=False): + """Return threshold value(s) based on ISODATA method. - Histogram-based threshold, known as Ridler-Calvard method or intermeans. + Histogram-based threshold, known as Ridler-Calvard method or inter-means. + Threshold values returned satisfy the following equality: + threshold = (image[image <= threshold].mean() + + image[image > threshold].mean()) / 2.0 + That is, returned thresholds are intensities that separate the image into + two groups of pixels, where the threshold intensity is midway between the + mean intensities of these groups. + + For integer images, the above equality holds to within one; for floating- + point images, the equality holds to within the histogram bin-width. Parameters ---------- @@ -205,12 +214,14 @@ def threshold_isodata(image, nbins=256): nbins : int, optional Number of bins used to calculate histogram. This value is ignored for integer arrays. + return_all: bool, optional + If False (default), return only the lowest threshold that satisfies + the above equality. If True, return all valid thresholds. Returns ------- - threshold : float or int, corresponding input array dtype. - Upper threshold value. All pixels intensities that less or equal of - this value assumed as background. + threshold : float, int, array + Threshold value(s). References ---------- @@ -232,27 +243,49 @@ def threshold_isodata(image, nbins=256): >>> thresh = threshold_isodata(image) >>> binary = image > thresh """ + hist, bin_centers = histogram(image, nbins) - # On blank images (e.g. filled with 0) with int dtype, `histogram()` - # returns `bin_centers` containing only one value. Speed up with it. - if bin_centers.size == 1: - return bin_centers[0] - # It is not necessary to calculate the probability mass function here, - # because the l and h fractions already include the normalization. - pmf = hist.astype(np.float32) # / hist.sum() - cpmfl = np.cumsum(pmf, dtype=np.float32) - cpmfh = np.cumsum(pmf[::-1], dtype=np.float32)[::-1] + hist = hist.astype(np.float32) + # csuml and csumh contain the count of pixels in that bin or lower, and + # in all bins strictly higher than that bin, respectively + csuml = np.cumsum(hist) + csumh = np.cumsum(hist[::-1])[::-1] - hist - binnums = np.arange(pmf.size, dtype=np.min_scalar_type(nbins)) - # l and h contain average value of pixels in sum of bins, calculated - # from lower to higher and from higher to lower respectively. - l = np.ma.divide(np.cumsum(pmf * binnums, dtype=np.float32), cpmfl) - h = np.ma.divide( - np.cumsum((pmf[::-1] * binnums[::-1]), dtype=np.float32)[::-1], - cpmfh) + # intensity_sum contains the total pixel intensity from each bin + intensity_sum = hist * bin_centers - allmean = (l + h) / 2.0 - threshold = bin_centers[np.nonzero(allmean.round() == binnums)[0][0]] - # This implementation returns threshold where - # `background <= threshold < foreground`. - return threshold + # l and h contain average value of all pixels in that bin or lower, and + # in all bins strictly higher than that bin, respectively. + # Note that since exp.histogram does not include empty bins at the low or + # high end of the range, csuml and csumh are strictly > 0, except in the + # last bin of csumh, which is zero by construction. + # So no worries about division by zero in the following lines, except + # for the last bin, but we can ignore that because no valid threshold + # can be in the top bin. So we just patch up csumh[-1] to not cause 0/0 + # errors. + csumh[-1] = 1 + l = np.cumsum(intensity_sum) / csuml + h = (np.cumsum(intensity_sum[::-1])[::-1] - intensity_sum) / csumh + + # isodata finds threshold values that meet the criterion t = (l + m)/2 + # where l is the mean of all pixels <= t and h is the mean of all pixels + # > t, as calculated above. So we are looking for places where + # (l + m) / 2 equals the intensity value for which those l and m figures + # were calculated -- which is, of course, the histogram bin centers. + # We only require this equality to be within the precision of the bin + # width, of course. + all_mean = (l + h) / 2.0 + bin_width = bin_centers[1] - bin_centers[0] + + # Look only at thresholds that are below the actual all_mean value, + # for consistency with the threshold being included in the lower pixel + # group. Otherwise can get thresholds that are not actually fixed-points + # of the isodata algorithm. For float images, this matters less, since + # there really can't be any guarantees anymore anyway. + distances = all_mean - bin_centers + thresholds = bin_centers[(distances >= 0) & (distances < bin_width)] + + if return_all: + return thresholds + else: + return thresholds[0] From 8aa5ef8697885d0a84dc0de28b44c3fce188bcef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 2 Aug 2014 09:10:50 -0400 Subject: [PATCH 0230/1122] Return to old function name threshold_isodata --- skimage/filter/thresholding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index 8a80ddb0..3426e0a8 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -193,7 +193,7 @@ def threshold_yen(image, nbins=256): return bin_centers[crit.argmax()] -def isodata(image, nbins=256, return_all=False): +def threshold_isodata(image, nbins=256, return_all=False): """Return threshold value(s) based on ISODATA method. Histogram-based threshold, known as Ridler-Calvard method or inter-means. From fc085ec779707145cdba9c888c608b524ce6a952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 2 Aug 2014 09:42:28 -0400 Subject: [PATCH 0231/1122] Account for case when image only contains one unique value --- skimage/filter/thresholding.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index 3426e0a8..0e908604 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -245,7 +245,16 @@ def threshold_isodata(image, nbins=256, return_all=False): """ hist, bin_centers = histogram(image, nbins) + + # image only contains one unique value + if len(bin_centers) == 1: + if return_all: + return bin_centers + else: + return bin_centers[0] + hist = hist.astype(np.float32) + # csuml and csumh contain the count of pixels in that bin or lower, and # in all bins strictly higher than that bin, respectively csuml = np.cumsum(hist) From ce0e9174c96e793474f7c44889d22155093e4b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 2 Aug 2014 09:45:36 -0400 Subject: [PATCH 0232/1122] Fix test cases which are now wrong due to previous binning error --- skimage/filter/tests/test_thresholding.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/filter/tests/test_thresholding.py b/skimage/filter/tests/test_thresholding.py index 8464cb21..11e15575 100644 --- a/skimage/filter/tests/test_thresholding.py +++ b/skimage/filter/tests/test_thresholding.py @@ -157,7 +157,7 @@ def test_yen_coins_image_as_float(): def test_isodata_camera_image(): camera = skimage.img_as_ubyte(data.camera()) - assert threshold_isodata(camera) == 88 + assert threshold_isodata(camera) == 87 def test_isodata_coins_image(): @@ -167,19 +167,19 @@ def test_isodata_coins_image(): def test_isodata_moon_image(): moon = skimage.img_as_ubyte(data.moon()) - assert threshold_isodata(moon) == 87 + assert threshold_isodata(moon) == 86 def test_isodata_moon_image_negative_int(): moon = skimage.img_as_ubyte(data.moon()).astype(np.int32) moon -= 100 - assert threshold_isodata(moon) == -13 + assert threshold_isodata(moon) == -14 def test_isodata_moon_image_negative_float(): moon = skimage.img_as_ubyte(data.moon()).astype(np.float64) moon -= 100 - assert -13 < threshold_isodata(moon) < -12 + assert -14 < threshold_isodata(moon) < -13 if __name__ == '__main__': From 23fd7b89aff9d8b2637ab39245d8617500ea64be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 2 Aug 2014 10:13:53 -0400 Subject: [PATCH 0233/1122] Improve test coverage --- skimage/filter/tests/test_thresholding.py | 68 +++++++++++++++++++---- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/skimage/filter/tests/test_thresholding.py b/skimage/filter/tests/test_thresholding.py index 11e15575..5b103e6d 100644 --- a/skimage/filter/tests/test_thresholding.py +++ b/skimage/filter/tests/test_thresholding.py @@ -1,5 +1,5 @@ import numpy as np -from numpy.testing import assert_array_equal +from numpy.testing import assert_equal, assert_almost_equal import skimage from skimage import data @@ -59,19 +59,25 @@ class TestSimpleImage(): def test_isodata(self): assert threshold_isodata(self.image) == 2 + assert threshold_isodata(self.image, return_all=True) == [2] def test_isodata_blank_zero(self): image = np.zeros((5, 5), dtype=np.uint8) assert threshold_isodata(image) == 0 + assert threshold_isodata(image, return_all=True) == [0] def test_isodata_linspace(self): - assert -63.8 < threshold_isodata(np.linspace(-127, 0, 256)) < -63.6 + image = np.linspace(-127, 0, 256) + assert -63.8 < threshold_isodata(image) < -63.6 + assert_almost_equal(threshold_isodata(image, return_all=True), + [-63.74804688, -63.25195312]) def test_isodata_16bit(self): np.random.seed(0) imfloat = np.random.rand(256, 256) - t = threshold_isodata(imfloat, nbins=1024) - assert 0.49 < t < 0.51 + assert 0.49 < threshold_isodata(imfloat, nbins=1024) < 0.51 + assert all(0.49 < threshold_isodata(imfloat, nbins=1024, + return_all=True)) def test_threshold_adaptive_generic(self): def func(arr): @@ -84,7 +90,7 @@ class TestSimpleImage(): [ True, True, False, False, False]] ) out = threshold_adaptive(self.image, 3, method='generic', param=func) - assert_array_equal(ref, out) + assert_equal(ref, out) def test_threshold_adaptive_gaussian(self): ref = np.array( @@ -95,7 +101,7 @@ class TestSimpleImage(): [ True, True, False, False, False]] ) out = threshold_adaptive(self.image, 3, method='gaussian') - assert_array_equal(ref, out) + assert_equal(ref, out) def test_threshold_adaptive_mean(self): ref = np.array( @@ -106,7 +112,7 @@ class TestSimpleImage(): [ True, True, False, False, False]] ) out = threshold_adaptive(self.image, 3, method='mean') - assert_array_equal(ref, out) + assert_equal(ref, out) def test_threshold_adaptive_median(self): ref = np.array( @@ -117,7 +123,7 @@ class TestSimpleImage(): [False, True, False, False, False]] ) out = threshold_adaptive(self.image, 3, method='median') - assert_array_equal(ref, out) + assert_equal(ref, out) def test_otsu_camera_image(): @@ -157,30 +163,68 @@ def test_yen_coins_image_as_float(): def test_isodata_camera_image(): camera = skimage.img_as_ubyte(data.camera()) - assert threshold_isodata(camera) == 87 + + threshold = threshold_isodata(camera) + assert np.floor((camera[camera <= threshold].mean() + + camera[camera > threshold].mean()) / 2.0) == threshold + assert threshold == 87 + + assert threshold_isodata(camera, return_all=True) == [87] def test_isodata_coins_image(): coins = skimage.img_as_ubyte(data.coins()) - assert threshold_isodata(coins) == 107 + + threshold = threshold_isodata(coins) + assert np.floor((coins[coins <= threshold].mean() + + coins[coins > threshold].mean()) / 2.0) == threshold + assert threshold == 107 + + assert threshold_isodata(coins, return_all=True) == [107] def test_isodata_moon_image(): moon = skimage.img_as_ubyte(data.moon()) - assert threshold_isodata(moon) == 86 + + threshold = threshold_isodata(moon) + assert np.floor((moon[moon <= threshold].mean() + + moon[moon > threshold].mean()) / 2.0) == threshold + assert threshold == 86 + + thresholds = threshold_isodata(moon, return_all=True) + for threshold in thresholds: + assert np.floor((moon[moon <= threshold].mean() + + moon[moon > threshold].mean()) / 2.0) == threshold + assert_equal(thresholds, [86, 87, 88, 122, 123, 124, 139, 140]) def test_isodata_moon_image_negative_int(): moon = skimage.img_as_ubyte(data.moon()).astype(np.int32) moon -= 100 - assert threshold_isodata(moon) == -14 + + threshold = threshold_isodata(moon) + assert np.floor((moon[moon <= threshold].mean() + + moon[moon > threshold].mean()) / 2.0) == threshold + assert threshold == -14 + + thresholds = threshold_isodata(moon, return_all=True) + for threshold in thresholds: + assert np.floor((moon[moon <= threshold].mean() + + moon[moon > threshold].mean()) / 2.0) == threshold + assert_equal(thresholds, [-14, -13, -12, 22, 23, 24, 39, 40]) def test_isodata_moon_image_negative_float(): moon = skimage.img_as_ubyte(data.moon()).astype(np.float64) moon -= 100 + assert -14 < threshold_isodata(moon) < -13 + thresholds = threshold_isodata(moon, return_all=True) + assert_almost_equal(thresholds, + [-13.83789062, -12.84179688, -11.84570312, 22.02148438, + 23.01757812, 24.01367188, 38.95507812, 39.95117188]) + if __name__ == '__main__': np.testing.run_module_suite() From 83a6b6484481712025862086bc0fc6df5afa1dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 2 Aug 2014 10:14:19 -0400 Subject: [PATCH 0234/1122] Fix return value description --- skimage/filter/thresholding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index 0e908604..3985e278 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -220,7 +220,7 @@ def threshold_isodata(image, nbins=256, return_all=False): Returns ------- - threshold : float, int, array + threshold : float or int or array Threshold value(s). References From f9fd683fcbd8a5eeee1e4424fcc0b9840452fb6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 8 Aug 2014 19:51:10 -0400 Subject: [PATCH 0235/1122] Test missed line for coverage --- skimage/filter/tests/test_thresholding.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skimage/filter/tests/test_thresholding.py b/skimage/filter/tests/test_thresholding.py index 5b103e6d..69b56cde 100644 --- a/skimage/filter/tests/test_thresholding.py +++ b/skimage/filter/tests/test_thresholding.py @@ -103,6 +103,9 @@ class TestSimpleImage(): out = threshold_adaptive(self.image, 3, method='gaussian') assert_equal(ref, out) + out = threshold_adaptive(self.image, 3, method='gaussian', param=1.0 / 3.0) + assert_equal(ref, out) + def test_threshold_adaptive_mean(self): ref = np.array( [[False, False, False, False, True], From 3973b3030039c05bedefba5e70464dfb66af2250 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 9 Aug 2014 21:29:41 +0530 Subject: [PATCH 0236/1122] docstring changes and code movement --- skimage/graph/_ncut_cy.pyx | 2 - skimage/graph/graph_cut.py | 84 +++++++++++++++++++++++++------------- skimage/graph/rag.py | 17 ++++---- 3 files changed, 64 insertions(+), 39 deletions(-) diff --git a/skimage/graph/_ncut_cy.pyx b/skimage/graph/_ncut_cy.pyx index 0888be72..de3b5c82 100644 --- a/skimage/graph/_ncut_cy.pyx +++ b/skimage/graph/_ncut_cy.pyx @@ -74,7 +74,5 @@ def cut_cost(cut, W): row = indices[row_index] if cut_mask[row] != cut_mask[col]: cost += data[row_index] - row_index += 1 - col += 1 return cost * 0.5 diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index b86f6c48..1e3d31dc 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -9,7 +9,7 @@ from . import _ncut_cy from scipy.sparse import linalg -def cut_threshold(labels, rag, thresh): +def cut_threshold(labels, rag, thresh, in_place=True): """Combine regions seperated by weight less than threshold. Given an image's labels and its RAG, output new labels by @@ -25,6 +25,10 @@ def cut_threshold(labels, rag, thresh): thresh : float The threshold. Regions connected by edges with smaller weights are combined. + in_place : bool + If set, modifies `rag` in place. The function will remove the edges + with weights less that `thresh`. If set to `False` the function + makes a copy of `rag` before proceeding. Returns ------- @@ -47,7 +51,9 @@ def cut_threshold(labels, rag, thresh): """ # Because deleting edges while iterating through them produces an error. - rag = rag.copy() + if not in_place: + rag = rag.copy() + to_remove = [(x, y) for x, y, d in rag.edges_iter(data=True) if d['weight'] >= thresh] rag.remove_edges_from(to_remove) @@ -66,7 +72,7 @@ def cut_threshold(labels, rag, thresh): return map_array[labels] -def cut_normalized(labels, rag, thresh=0.001, num_cuts=10): +def cut_normalized(labels, rag, thresh=0.001, num_cuts=10, in_place=True): """Perform Normalized Graph cut on the Region Adjacency Graph. Given an image's labels and its similarity RAG, recursively perform @@ -85,6 +91,9 @@ def cut_normalized(labels, rag, thresh=0.001, num_cuts=10): value of the N-cut exceeds `thresh`. num_cuts : int The number or N-cuts to perform before determining the optimal one. + in_place : bool + If set, modifies `rag` in place. For each node `n` the function will + set a new attribute ``rag.node[n]['ncut label]``. Returns ------- @@ -106,8 +115,15 @@ def cut_normalized(labels, rag, thresh=0.001, num_cuts=10): IEEE Transactions on , vol.22, no.8, pp.888,905, August 2000 """ - map_array = np.arange(labels.max() + 1) - _ncut_relabel(rag, thresh, num_cuts, map_array) + if not in_place: + rag = rag.copy() + + _ncut_relabel(rag, thresh, num_cuts) + + map_array = np.zeros(labels.max() + 1) + # Mapping from old labels to new + for n, d in rag.nodes_iter(data=True): + map_array[d['labels']] = d['ncut label'] return map_array[labels] @@ -128,6 +144,16 @@ def partition_by_cut(cut, rag): sub1, sub2 : RAG The two resulting subgraphs from the bi-partition. """ + # `cut` is derived from `D` and `W` matrices, which also follow the + # ordering returned by `rag.nodes()` because we use + # nx.to_scipy_sparce_matrix. + + # Example + # rag.nodes() = [3, 7, 9, 13] + # cut = [True, False, True, False] + # nodes1 = [3, 9] + # nodes2 = [7, 10] + nodes1 = [n for i, n in enumerate(rag.nodes()) if cut[i]] nodes2 = [n for i, n in enumerate(rag.nodes()) if not cut[i]] @@ -153,9 +179,10 @@ def get_min_ncut(ev, d, w, num_cuts): Returns ------- - threshold, mcut : float - The threshold which produced the minimum ncut, and the value of the - ncut itself. + mask : array + The array of booleans which denotes the bi-partition. + mcut : float + The value of the minimum ncut. """ mcut = np.inf @@ -165,13 +192,21 @@ def get_min_ncut(ev, d, w, num_cuts): mask = ev > t cost = _ncut.ncut_cost(mask, d, w) if cost < mcut: + min_mask = mask mcut = cost - threshold = t - return threshold, mcut + return min_mask, mcut -def _ncut_relabel(rag, thresh, num_cuts, map_array): +def _label_all(rag, attr_name): + node = rag.nodes()[0] + new_label = rag.node[node]['labels'][0] + for n, d in rag.nodes_iter(data=True): + for l in d['labels']: + d[attr_name] = new_label + + +def _ncut_relabel(rag, thresh, num_cuts): """Perform Normalized Graph cut on the Region Adjacency Graph. Recursively partition the graph into 2, until further subdivision @@ -195,46 +230,37 @@ def _ncut_relabel(rag, thresh, num_cuts, map_array): the function. """ d, w = _ncut.DW_matrices(rag) - stop = False m = w.shape[0] if m > 2: d2 = d.copy() # Since d is diagonal, we can directly operate on it's data - # the inverse - d2.data = 1.0 / d2.data - # the square root - d2.data = np.sqrt(d2.data) + # the inverse of the square root + d2.data = np.reciprocal(np.sqrt(d2.data, out=d2.data), out=d2.data) + # Refer Shi & Malik 2001, Equation 7, Page 891 vals, vectors = linalg.eigsh(d2 * (d - w) * d2, which='SM', k=min(100, m - 2)) - else: - stop = True - if not stop: - # Pick second smalles eigenvector. + # Pick second smallest eigenvector. # Refer Shi & Malik 2001, Section 3.2.3, Page 893 vals, vectors = np.real(vals), np.real(vectors) index2 = _ncut_cy.argmin2(vals) ev = _ncut.normalize(vectors[:, index2]) - threshold, mcut = get_min_ncut(ev, d, w, num_cuts) + cut_mask, mcut = get_min_ncut(ev, d, w, num_cuts) if (mcut < thresh): - cut_mask = ev > threshold # Sub divide and perform N-cut again # Refer Shi & Malik 2001, Section 3.2.5, Page 893 sub1, sub2 = partition_by_cut(cut_mask, rag) - _ncut_relabel(sub1, thresh, num_cuts, map_array) - _ncut_relabel(sub2, thresh, num_cuts, map_array) + _ncut_relabel(sub1, thresh, num_cuts) + _ncut_relabel(sub2, thresh, num_cuts) return # The N-cut wasn't small enough, or could not be computed. # The remaining graph is a region. # Assign `ncut label` by picking any label from the existing nodes, since # `labels` are unique, `new_label` is also unique. - node = rag.nodes()[0] - new_label = rag.node[node]['labels'][0] - for n, d in rag.nodes_iter(data=True): - for l in d['labels']: - map_array[l] = new_label + + _label_all(rag, 'ncut label') diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index f33936c2..1366a6d9 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -2,6 +2,7 @@ try: import networkx as nx except ImportError: msg = "Graph functions require networkx, which is not installed" + class nx: class Graph: def __init__(self, *args, **kwargs): @@ -119,7 +120,7 @@ def _add_edge_filter(values, graph): return 0 -def rag_mean_color(image, labels, connectivity=2, mode='dissimilarity', +def rag_mean_color(image, labels, connectivity=2, mode='distance', sigma=255.0): """Compute the Region Adjacency Graph using mean colors. @@ -142,15 +143,15 @@ def rag_mean_color(image, labels, connectivity=2, mode='dissimilarity', are considered adjacent. It can range from 1 to `labels.ndim`. Its behavior is the same as `connectivity` parameter in `scipy.ndimage.filters.generate_binary_structure`. - mode : str['similarity' | 'dissimilarity'] + mode : {'distance', 'similarity'}, optional The strategy to assign edge weights. - 'similarity' : The weight between two adjacent regions is the + 'distance' : The weight between two adjacent regions is the :math:`|c_1 - c_2|`, where :math:`c_1` and :math:`c_2` are the mean - colors of the two regions. It represents how different two regions - are. + colors of the two regions. It represents the Euclidian distance in + their average color. - 'dissimilarity' : The weight between two adjacent is + 'similarity' : The weight between two adjacent is :math:`e^{-d^2/sigma}` where :math:`d=|c_1 - c_2|`, where :math:`c_1` and :math:`c_2` are the mean colors of the two regions. It represents how similar two regions are. @@ -229,9 +230,9 @@ def rag_mean_color(image, labels, connectivity=2, mode='dissimilarity', diff = np.linalg.norm(diff) if mode == 'similarity': d['weight'] = math.e ** (-(diff ** 2) / sigma) - elif mode == 'dissimilarity': + elif mode == 'distance': d['weight'] = diff else: - raise ValueError("The mode '%s' is not recodnized" % mode) + raise ValueError("The mode '%s' is not recognised" % mode) return graph From 05380823cc464c9edac48f198b7b7648ae9ea7eb Mon Sep 17 00:00:00 2001 From: Adam Feuer Date: Sat, 9 Aug 2014 09:01:07 -0700 Subject: [PATCH 0237/1122] updated imread parameters comment to standard format - with 'str' file type (instead of 'string') --- skimage/io/_plugins/pil_plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 9e7bb499..7e5e90b2 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -21,7 +21,8 @@ def imread(fname, dtype=None): Parameters ---------- - fname : file name as string + fname : str + File name. dtype : numpy dtype object or string specifier Specifies data type of array elements. From 25f08ff68edfd531ac7ea04c8427e6b85eaacd2d Mon Sep 17 00:00:00 2001 From: Adam Feuer Date: Sat, 9 Aug 2014 09:03:06 -0700 Subject: [PATCH 0238/1122] removing trailing space (PEP8 formatting) --- skimage/io/_plugins/pil_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 7e5e90b2..917c49dc 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -41,7 +41,7 @@ def pil_to_ndarray(im, dtype=None): """ - fp = im.fp if hasattr(im, 'fp') else None + fp = im.fp if hasattr(im, 'fp') else None if im.mode == 'P': if _palette_is_grayscale(im): im = im.convert('L') From ef7da3be4a24272539f33d0f0376acfbf79562c4 Mon Sep 17 00:00:00 2001 From: Adam Feuer Date: Sat, 9 Aug 2014 09:04:05 -0700 Subject: [PATCH 0239/1122] explicitly checking for None as per code review feedback - instead of implicit (non-truthy) test --- skimage/io/_plugins/pil_plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 917c49dc..406cafba 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -57,8 +57,8 @@ def pil_to_ndarray(im, dtype=None): elif 'A' in im.mode: im = im.convert('RGBA') im = np.array(im, dtype=dtype) - if fp: - fp.close() + if fp is not None: + fp.close() return im From ded138cbcb3040beea4947f800ac3bb306200a11 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 9 Aug 2014 21:35:07 +0530 Subject: [PATCH 0240/1122] test case for in place --- skimage/graph/tests/test_rag.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 1e643fc4..0e62aa1a 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -64,11 +64,15 @@ def test_threshold_cut(): labels[50:, 50:] = 3 rag = graph.rag_mean_color(img, labels) - new_labels = graph.cut_threshold(labels, rag, 10) - + new_labels = graph.cut_threshold(labels, rag, 10, in_place=False) # Two labels assert new_labels.max() == 1 + new_labels = graph.cut_threshold(labels, rag, 10) + # Two labels + assert new_labels.max() == 1 + + @skipif(not is_installed('networkx')) def test_cut_normalized(): @@ -85,14 +89,20 @@ def test_cut_normalized(): labels[50:, 50:] = 3 rag = graph.rag_mean_color(img, labels, mode='similarity') - new_labels = graph.cut_normalized(labels, rag) - new_labels, _, _ = segmentation.relabel_sequential(new_labels) + new_labels = graph.cut_normalized(labels, rag, in_place=False) + new_labels, _, _ = segmentation.relabel_sequential(new_labels) # Two labels assert new_labels.max() == 1 + new_labels = graph.cut_normalized(labels, rag) + new_labels, _, _ = segmentation.relabel_sequential(new_labels) + assert new_labels.max() == 1 + + @skipif(not is_installed('networkx')) def test_rag_error(): img = np.zeros((10, 10, 3), dtype='uint8') labels = np.zeros((10, 10), dtype='uint8') - testing.assert_raises(ValueError, graph.rag_mean_color, img, labels, 2, 'non existant mode') + testing.assert_raises(ValueError, graph.rag_mean_color, img, labels, + 2, 'non existant mode') From 03d3873cc43cb2f9725da27fd85d8704625a64af Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 9 Aug 2014 22:08:27 +0530 Subject: [PATCH 0241/1122] docstring of _label_all --- skimage/graph/graph_cut.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 1e3d31dc..f69df469 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -199,6 +199,17 @@ def get_min_ncut(ev, d, w, num_cuts): def _label_all(rag, attr_name): + """Assign a uique integer to the given attribute in the RAG. + + This function assumes that all labels in `rag` are unique. It + picks up a random label from them and assigns it to the `attr_name` + attribute of all the nodes. + + rag : RAG + The Region Adjacency Graph. + attr_name : string + The attribute to which a unique integer is assigned. + """ node = rag.nodes()[0] new_label = rag.node[node]['labels'][0] for n, d in rag.nodes_iter(data=True): @@ -262,5 +273,4 @@ def _ncut_relabel(rag, thresh, num_cuts): # The remaining graph is a region. # Assign `ncut label` by picking any label from the existing nodes, since # `labels` are unique, `new_label` is also unique. - _label_all(rag, 'ncut label') From 0c185629d3002ffcb9d5306c7f848243585f8341 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 9 Aug 2014 22:09:12 +0530 Subject: [PATCH 0242/1122] corrected similarity --- skimage/graph/rag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 1366a6d9..c1108aa2 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -156,7 +156,7 @@ def rag_mean_color(image, labels, connectivity=2, mode='distance', :math:`c_1` and :math:`c_2` are the mean colors of the two regions. It represents how similar two regions are. sigma : float, optional - Used for computation when `mode` is "dissimilarity". It governs how + Used for computation when `mode` is "similarity". It governs how close to each other two colors should be, for their corresponding edge weight to be significant. A very large value of `sigma` could make any two colors behave as though they were similar. From 8494b8081afb8362e142aa426c619aeb3f6d5e05 Mon Sep 17 00:00:00 2001 From: Adam Feuer Date: Sat, 9 Aug 2014 10:37:11 -0700 Subject: [PATCH 0243/1122] removed trailing blank line in comment --- skimage/io/_plugins/pil_plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 406cafba..eb6467df 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -39,7 +39,6 @@ def pil_to_ndarray(im, dtype=None): ---------- Refer to ``imread``. - """ fp = im.fp if hasattr(im, 'fp') else None if im.mode == 'P': From 4a231cfd3587e6e043aafd7aab59df28e185a217 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sun, 10 Aug 2014 20:07:34 +0530 Subject: [PATCH 0244/1122] Minor changes --- skimage/graph/graph_cut.py | 13 ++++++------- skimage/graph/rag.py | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index f69df469..56939b7e 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -50,10 +50,10 @@ def cut_threshold(labels, rag, thresh, in_place=True): http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.11.5274 """ - # Because deleting edges while iterating through them produces an error. if not in_place: rag = rag.copy() + # Because deleting edges while iterating through them produces an error. to_remove = [(x, y) for x, y, d in rag.edges_iter(data=True) if d['weight'] >= thresh] rag.remove_edges_from(to_remove) @@ -93,7 +93,7 @@ def cut_normalized(labels, rag, thresh=0.001, num_cuts=10, in_place=True): The number or N-cuts to perform before determining the optimal one. in_place : bool If set, modifies `rag` in place. For each node `n` the function will - set a new attribute ``rag.node[n]['ncut label]``. + set a new attribute ``rag.node[n]['ncut label']``. Returns ------- @@ -146,7 +146,7 @@ def partition_by_cut(cut, rag): """ # `cut` is derived from `D` and `W` matrices, which also follow the # ordering returned by `rag.nodes()` because we use - # nx.to_scipy_sparce_matrix. + # nx.to_scipy_sparse_matrix. # Example # rag.nodes() = [3, 7, 9, 13] @@ -199,7 +199,7 @@ def get_min_ncut(ev, d, w, num_cuts): def _label_all(rag, attr_name): - """Assign a uique integer to the given attribute in the RAG. + """Assign a unique integer to the given attribute in the RAG. This function assumes that all labels in `rag` are unique. It picks up a random label from them and assigns it to the `attr_name` @@ -213,8 +213,7 @@ def _label_all(rag, attr_name): node = rag.nodes()[0] new_label = rag.node[node]['labels'][0] for n, d in rag.nodes_iter(data=True): - for l in d['labels']: - d[attr_name] = new_label + d[attr_name] = new_label def _ncut_relabel(rag, thresh, num_cuts): @@ -245,7 +244,7 @@ def _ncut_relabel(rag, thresh, num_cuts): if m > 2: d2 = d.copy() - # Since d is diagonal, we can directly operate on it's data + # Since d is diagonal, we can directly operate on its data # the inverse of the square root d2.data = np.reciprocal(np.sqrt(d2.data, out=d2.data), out=d2.data) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index c1108aa2..5ef9a83b 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -148,7 +148,7 @@ def rag_mean_color(image, labels, connectivity=2, mode='distance', 'distance' : The weight between two adjacent regions is the :math:`|c_1 - c_2|`, where :math:`c_1` and :math:`c_2` are the mean - colors of the two regions. It represents the Euclidian distance in + colors of the two regions. It represents the Euclidean distance in their average color. 'similarity' : The weight between two adjacent is From ab60edcdf73f3c7a4e5e0b91866ec1e5a08e8295 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sun, 10 Aug 2014 20:31:22 +0530 Subject: [PATCH 0245/1122] Fix Reference format --- skimage/graph/graph_cut.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 56939b7e..97ca7507 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -112,7 +112,7 @@ def cut_normalized(labels, rag, thresh=0.001, num_cuts=10, in_place=True): ---------- .. [1] Shi, J.; Malik, J., "Normalized cuts and image segmentation", Pattern Analysis and Machine Intelligence, - IEEE Transactions on , vol.22, no.8, pp.888,905, August 2000 + IEEE Transactions on, vol. 22, no. 8, pp. 888-905, August 2000. """ if not in_place: From 7bd1da86f28bc5dd150bb83e47ed46991cf94bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 11 Aug 2014 09:02:31 -0400 Subject: [PATCH 0246/1122] Remove legacy CI config --- tox.ini | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 tox.ini diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 02feddba..00000000 --- a/tox.ini +++ /dev/null @@ -1,10 +0,0 @@ -[testenv] -deps = nose - coverage - cython - numpy - scipy -commands= - nosetests {posargs:--with-coverage --cover-package=skimage} - coverage xml - From 72f6a8c0567cc0637ddbe3da28cfe8c1a81988d6 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 10 Aug 2014 20:09:39 -0500 Subject: [PATCH 0247/1122] Make suggested changes to build update and use travis_retry for all installs. --- .travis.yml | 12 ++++++------ skimage/_shared/LICENSE.txt | 12 ++++++++++++ skimage/_shared/version_requirements.py | 2 +- skimage/viewer/canvastools/painttool.py | 2 +- 4 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 skimage/_shared/LICENSE.txt diff --git a/.travis.yml b/.travis.yml index 3a061e13..a2c99f97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,7 +32,7 @@ before_install: sudo apt-get install python3-numpy; wget https://raw.githubusercontent.com/numpy/numpy/master/numpy/_import_tools.py -O /home/travis/virtualenv/python3.2_with_system_site_packages/lib/python3.2/site-packages/numpy/_import_tools.py; sudo apt-get install python3-scipy; - pip install cython flake8 six; + travis_retry pip install cython flake8 six; else wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; bash miniconda.sh -b -p $HOME/miniconda; @@ -44,7 +44,7 @@ before_install: travis_retry conda create -n test $ENV six scipy pip flake8 nose; source activate test; fi - - pip install coveralls pillow + - travis_retry pip install coveralls pillow - python check_bento_build.py install: @@ -69,16 +69,16 @@ script: # https://groups.google.com/a/continuum.io/forum/#!topic/anaconda/-DLG2ZdTkw0 - if [[ $ENV == python=3.2 ]]; then sudo apt-get install -qq python3-pyqt4; - pip install --use-mirrors -q matplotlib; - pip install -q networkx; + travis_retry pip install -q matplotlib; + travis_retry pip install -q networkx; else travis_retry conda install -q matplotlib pyqt networkx; sudo cp ~/miniconda/envs/test/include/png* /usr/include; rm ~/miniconda/envs/test/lib/libm-2.5.so; rm ~/miniconda/envs/test/lib/libm.*; sudo apt-get install -qq libtiff4-dev libwebp-dev xcftools; - pip install -q imread; - pip install -q pyfits; + travis_retry pip install -q imread; + travis_retry pip install -q pyfits; fi - sudo apt-get install -qq libfreeimage3 # TODO: update when SimpleITK become available on py34 or hopefully pip diff --git a/skimage/_shared/LICENSE.txt b/skimage/_shared/LICENSE.txt new file mode 100644 index 00000000..69293997 --- /dev/null +++ b/skimage/_shared/LICENSE.txt @@ -0,0 +1,12 @@ + +version_requirements._check_version: + + Copyright (C) 2013 The IPython Development Team + + Distributed under the terms of the BSD License. + + +version_requirements.is_installed: + + Original Copyright (C) 2009-2011 Pierre Raybaut + Licensed under the terms of the MIT License. diff --git a/skimage/_shared/version_requirements.py b/skimage/_shared/version_requirements.py index 4ebf8ee9..430f7f5e 100644 --- a/skimage/_shared/version_requirements.py +++ b/skimage/_shared/version_requirements.py @@ -55,7 +55,7 @@ def is_installed(name, version=None): Returns ------- out : bool - True if *name* is installed matching the optional version. + True if `name` is installed matching the optional version. Note ---- diff --git a/skimage/viewer/canvastools/painttool.py b/skimage/viewer/canvastools/painttool.py index fd994a10..9b94e0a2 100644 --- a/skimage/viewer/canvastools/painttool.py +++ b/skimage/viewer/canvastools/painttool.py @@ -3,7 +3,7 @@ try: import matplotlib.pyplot as plt import matplotlib.colors as mcolors LABELS_CMAP = mcolors.ListedColormap(['white', 'red', 'dodgerblue', 'gold', - 'greenyellow', 'blueviolet']) + 'greenyellow', 'blueviolet']) except ImportError: pass from skimage.viewer.canvastools.base import CanvasToolBase From 6ef01cb4dca2ddc84e99f5c0117cf05dce2341b0 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 13 Aug 2014 02:12:51 +0530 Subject: [PATCH 0248/1122] change ref format in example --- doc/examples/plot_ncut.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/examples/plot_ncut.py b/doc/examples/plot_ncut.py index 3160f710..6b8a62ab 100644 --- a/doc/examples/plot_ncut.py +++ b/doc/examples/plot_ncut.py @@ -10,7 +10,7 @@ References ---------- .. [1] Shi, J.; Malik, J., "Normalized cuts and image segmentation", Pattern Analysis and Machine Intelligence, - IEEE Transactions on , vol.22, no.8, pp.888,905, Aug 2000 + IEEE Transactions on, vol. 22, no. 8, pp. 888-905, August 2000. """ from skimage import graph, data, io, segmentation, color from matplotlib import pyplot as plt From b61763117d4bcf4ad30de6b2f67cbfb3d58e2ca1 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 14 Aug 2014 23:45:30 +0530 Subject: [PATCH 0249/1122] Update _ncut_cy.pyx --- skimage/graph/_ncut_cy.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/graph/_ncut_cy.pyx b/skimage/graph/_ncut_cy.pyx index de3b5c82..14f3a94a 100644 --- a/skimage/graph/_ncut_cy.pyx +++ b/skimage/graph/_ncut_cy.pyx @@ -11,7 +11,7 @@ def argmin2(cnp.double_t[:] array): Parameters ---------- - a : array + array : array The array to process. Returns From afa345f363151f72d42976090ad16bac008f4cce Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 14 Aug 2014 23:47:00 +0530 Subject: [PATCH 0250/1122] Update graph_cut.py --- skimage/graph/graph_cut.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 97ca7507..3f973e22 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -77,7 +77,7 @@ def cut_normalized(labels, rag, thresh=0.001, num_cuts=10, in_place=True): Given an image's labels and its similarity RAG, recursively perform a 2-way normalized cut on it. All nodes belonging to a subgraph - which cannot be cut further, are assigned a unique label in the + that cannot be cut further are assigned a unique label in the output. Parameters From 39dfcc414499f7a84ebf53900b93ae192f4eb54f Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Tue, 29 Jul 2014 01:05:10 +0530 Subject: [PATCH 0251/1122] Frist working copy of RAG draw --- doc/examples/plot_rag_draw.py | 21 ++++++++++++++ skimage/graph/rag.py | 53 +++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 doc/examples/plot_rag_draw.py diff --git a/doc/examples/plot_rag_draw.py b/doc/examples/plot_rag_draw.py new file mode 100644 index 00000000..22fb9e63 --- /dev/null +++ b/doc/examples/plot_rag_draw.py @@ -0,0 +1,21 @@ +from skimage import graph, data, io, segmentation, color +from matplotlib import pyplot as plt + + +img = data.coffee() + +labels = segmentation.slic(img, compactness=30, n_segments=400) + +g = graph.rag_mean_color(img, labels) + +out = graph.rag.rag_draw(labels, g, img) +plt.figure() +plt.title("RAG with all edges shown in green.") +plt.imshow(out) + +out = graph.rag.rag_draw(labels, g, img, high_color=(1,0,0), thresh=30) +plt.figure() +plt.title("RAG with edge weights less than 30, color mapped between green and red.") +plt.imshow(out) + +plt.show() diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 5ef9a83b..42e4c4e3 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -13,6 +13,7 @@ import numpy as np from scipy.ndimage import filters from scipy import ndimage as nd import math +from .. import draw, measure, segmentation def min_weight(graph, src, dst, n): @@ -236,3 +237,55 @@ def rag_mean_color(image, labels, connectivity=2, mode='distance', raise ValueError("The mode '%s' is not recognised" % mode) return graph + + + +def rag_draw(labels, rag, img, border_color = (0,0,0), node_color = (1,1,0), + low_color = (0,1,0), high_color=None, thresh=np.inf ): + rag = rag.copy() + rag_labels = labels.copy() + out = img.copy() + + low_color = np.array(low_color) + + if not high_color is None : + high_color = np.array(high_color) + + # Handling the case where one node has multiple labels + offset = 1 + for n, d in rag.nodes_iter(data=True): + for l in d['labels'] : + rag_labels[labels == l] = offset + offset += 1 + + regions = measure.regionprops(rag_labels) + for region in regions: + # Because we kept the offset as 1 + rag.node[region['label'] - 1]['centroid'] = region['centroid'] + + if not border_color is None : + out = segmentation.mark_boundaries(out, rag_labels, color = border_color) + + if not high_color is None : + max_weight = max([d['weight'] for x, y, d in rag.edges_iter(data=True) if d['weight'] < thresh ]) + min_weight = min([d['weight'] for x, y, d in rag.edges_iter(data=True) if d['weight'] < thresh ]) + + for n1,n2,data in rag.edges_iter(data=True): + + if data['weight'] >= thresh : + continue + r1, c1 = map(int, rag.node[n1]['centroid']) + r2, c2 = map(int, rag.node[n2]['centroid']) + + line = draw.line(r1, c1, r2, c2) + + if not high_color is None: + norm_weight = ( rag[n1][n2]['weight'] - min_weight ) / ( max_weight - min_weight ) + out[line] = norm_weight*high_color + (1 - norm_weight)*low_color + else: + out[line] = low_color + + circle = draw.circle(r1,c1,2) + out[circle] = node_color + + return out From f4aa0fc8e16d8c3ec24759441b8150aea98c0240 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Tue, 29 Jul 2014 02:36:20 +0530 Subject: [PATCH 0252/1122] Formatting --- doc/examples/plot_rag_draw.py | 7 ++--- skimage/graph/rag.py | 48 ++++++++++++++++++++--------------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/doc/examples/plot_rag_draw.py b/doc/examples/plot_rag_draw.py index 22fb9e63..e554e1a7 100644 --- a/doc/examples/plot_rag_draw.py +++ b/doc/examples/plot_rag_draw.py @@ -1,4 +1,4 @@ -from skimage import graph, data, io, segmentation, color +from skimage import graph, data, segmentation from matplotlib import pyplot as plt @@ -13,9 +13,10 @@ plt.figure() plt.title("RAG with all edges shown in green.") plt.imshow(out) -out = graph.rag.rag_draw(labels, g, img, high_color=(1,0,0), thresh=30) +out = graph.rag.rag_draw(labels, g, img, high_color=(1, 0, 0), thresh=30) plt.figure() -plt.title("RAG with edge weights less than 30, color mapped between green and red.") +plt.title("RAG with edge weights less than 30,\ + color mapped between green and red.") plt.imshow(out) plt.show() diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 42e4c4e3..5a3db12a 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -239,22 +239,21 @@ def rag_mean_color(image, labels, connectivity=2, mode='distance', return graph - -def rag_draw(labels, rag, img, border_color = (0,0,0), node_color = (1,1,0), - low_color = (0,1,0), high_color=None, thresh=np.inf ): +def rag_draw(labels, rag, img, border_color=(0, 0, 0), node_color = (1, 1, 0), + low_color = (0, 1, 0), high_color=None, thresh=np.inf): rag = rag.copy() rag_labels = labels.copy() out = img.copy() - + low_color = np.array(low_color) - - if not high_color is None : + + if not high_color is None: high_color = np.array(high_color) # Handling the case where one node has multiple labels offset = 1 for n, d in rag.nodes_iter(data=True): - for l in d['labels'] : + for l in d['labels']: rag_labels[labels == l] = offset offset += 1 @@ -263,29 +262,36 @@ def rag_draw(labels, rag, img, border_color = (0,0,0), node_color = (1,1,0), # Because we kept the offset as 1 rag.node[region['label'] - 1]['centroid'] = region['centroid'] - if not border_color is None : - out = segmentation.mark_boundaries(out, rag_labels, color = border_color) + if not border_color is None: + out = segmentation.mark_boundaries( + out, + rag_labels, + color=border_color) - if not high_color is None : - max_weight = max([d['weight'] for x, y, d in rag.edges_iter(data=True) if d['weight'] < thresh ]) - min_weight = min([d['weight'] for x, y, d in rag.edges_iter(data=True) if d['weight'] < thresh ]) - - for n1,n2,data in rag.edges_iter(data=True): + if not high_color is None: + max_weight = max([d['weight'] for x, y, d in rag.edges_iter(data=True) + if d['weight'] < thresh]) + min_weight = min([d['weight'] for x, y, d in rag.edges_iter(data=True) + if d['weight'] < thresh]) - if data['weight'] >= thresh : + for n1, n2, data in rag.edges_iter(data=True): + + if data['weight'] >= thresh: continue r1, c1 = map(int, rag.node[n1]['centroid']) r2, c2 = map(int, rag.node[n2]['centroid']) - line = draw.line(r1, c1, r2, c2) - + line = draw.line(r1, c1, r2, c2) + if not high_color is None: - norm_weight = ( rag[n1][n2]['weight'] - min_weight ) / ( max_weight - min_weight ) - out[line] = norm_weight*high_color + (1 - norm_weight)*low_color + norm_weight = (rag[n1][n2]['weight'] - min_weight) / ( + max_weight - min_weight) + out[line] = norm_weight * high_color + \ + (1 - norm_weight) * low_color else: out[line] = low_color - - circle = draw.circle(r1,c1,2) + + circle = draw.circle(r1, c1, 2) out[circle] = node_color return out From 5c662b447227ed422bdbf2b1ae51602d0c846780 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Tue, 29 Jul 2014 03:12:22 +0530 Subject: [PATCH 0253/1122] Docstrings --- doc/examples/plot_rag_draw.py | 16 +++++++--- skimage/graph/__init__.py | 4 +++ skimage/graph/rag.py | 60 ++++++++++++++++++++++++++++++----- 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/doc/examples/plot_rag_draw.py b/doc/examples/plot_rag_draw.py index e554e1a7..1c47743e 100644 --- a/doc/examples/plot_rag_draw.py +++ b/doc/examples/plot_rag_draw.py @@ -1,22 +1,28 @@ +""" +=========== +RAG Drawing +=========== + +This example constructs a Region Adjacency Graph (RAG) and draws it with +the `rag_draw` method. +""" from skimage import graph, data, segmentation from matplotlib import pyplot as plt img = data.coffee() - labels = segmentation.slic(img, compactness=30, n_segments=400) - g = graph.rag_mean_color(img, labels) -out = graph.rag.rag_draw(labels, g, img) +out = graph.rag_draw(labels, g, img) plt.figure() plt.title("RAG with all edges shown in green.") plt.imshow(out) -out = graph.rag.rag_draw(labels, g, img, high_color=(1, 0, 0), thresh=30) +out = graph.rag_draw(labels, g, img, high_color=(1, 0, 0), thresh=30) plt.figure() plt.title("RAG with edge weights less than 30,\ color mapped between green and red.") -plt.imshow(out) +plt.imshow(out) plt.show() diff --git a/skimage/graph/__init__.py b/skimage/graph/__init__.py index aca73bc4..2f1162d3 100644 --- a/skimage/graph/__init__.py +++ b/skimage/graph/__init__.py @@ -2,8 +2,11 @@ from .spath import shortest_path from .mcp import MCP, MCP_Geometric, MCP_Connect, MCP_Flexible, route_through_array from .rag import rag_mean_color, RAG from .graph_cut import cut_threshold, cut_normalized +from .rag import rag_mean_color, RAG, rag_draw +from .graph_cut import cut_threshold ncut = cut_normalized + __all__ = ['shortest_path', 'MCP', 'MCP_Geometric', @@ -14,4 +17,5 @@ __all__ = ['shortest_path', 'cut_threshold', 'cut_normalized', 'ncut', + 'rag_draw', 'RAG'] diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 5a3db12a..d01d6e12 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -241,6 +241,52 @@ def rag_mean_color(image, labels, connectivity=2, mode='distance', def rag_draw(labels, rag, img, border_color=(0, 0, 0), node_color = (1, 1, 0), low_color = (0, 1, 0), high_color=None, thresh=np.inf): + """Draw a Region Adjacency Graph on an image. + + Given a labelled image and its corresponding RAG, draw the nodes and edges + of the RAG on the image with the specified colors. Nodes are markes by + the centroids of the corresposning regions. + + Parameters + ---------- + labels : ndarray, shape(M, N, [..., P,]) + The labelled image. This should have one dimension less than + `img`. If `image` has dimensions `(M, N, 3)` `labels` should have + dimensions `(M, N)`. + rag : RAG + The Region Adjacency Graph. + img : ndarray, shape(M, N, [..., P,] 3) + Input image. + border_color : length-3 sequence, optional + RGB color of the corder of regions. Specifying `None` won't draw + the border. + node_color : length-3 sequeunce, optional + RGB color of the centroid of nodes. Yellow by default. + low_color : length-3 sequeunce, optional + RGB color of the edges. If `high_color` is not specified, all edges + are draw with `low_color`. Green by default. + high_color : length-3 sequeunce, optional + RGB color of the edges with high weight. If specified, the edges are + color mapped between `low_color` and `high_color` depending on their + weight. Edges with low weights are more like `low_color` whereas edges + with high weights are more like `high_color`. + thresh : float, optiona; + Edges with weight below `thresh` are not drawn, or considered for color + mapping in case `high_color` is specified. + + Returns + ------- + out : ndarray, shape(M, N, [..., P,] 3) + The image with the RAG drawn. + + Examples + -------- + >>> from skimage import data, graph, segmentation + >>> img = data.lena() + >>> labels = segmentation.slic(img) + >>> g = graph.rag_mean_color(img, labels) + >>> out = graph.rag_draw(labels, g, img) + """ rag = rag.copy() rag_labels = labels.copy() out = img.copy() @@ -251,6 +297,7 @@ def rag_draw(labels, rag, img, border_color=(0, 0, 0), node_color = (1, 1, 0), high_color = np.array(high_color) # Handling the case where one node has multiple labels + # offset is 1 so that regionprops does not ignore 0 offset = 1 for n, d in rag.nodes_iter(data=True): for l in d['labels']: @@ -263,10 +310,7 @@ def rag_draw(labels, rag, img, border_color=(0, 0, 0), node_color = (1, 1, 0), rag.node[region['label'] - 1]['centroid'] = region['centroid'] if not border_color is None: - out = segmentation.mark_boundaries( - out, - rag_labels, - color=border_color) + out = segmentation.mark_boundaries(out, rag_labels, color=border_color) if not high_color is None: max_weight = max([d['weight'] for x, y, d in rag.edges_iter(data=True) @@ -284,10 +328,10 @@ def rag_draw(labels, rag, img, border_color=(0, 0, 0), node_color = (1, 1, 0), line = draw.line(r1, c1, r2, c2) if not high_color is None: - norm_weight = (rag[n1][n2]['weight'] - min_weight) / ( - max_weight - min_weight) - out[line] = norm_weight * high_color + \ - (1 - norm_weight) * low_color + norm_weight = ((rag[n1][n2]['weight'] - min_weight) / + (max_weight - min_weight)) + out[line] = (norm_weight * high_color + + (1 - norm_weight) * low_color) else: out[line] = low_color From 6928e5d9ef3c85fbd18ed6f008d55007947e4f36 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Tue, 29 Jul 2014 03:15:24 +0530 Subject: [PATCH 0254/1122] Corrected docstrings --- skimage/graph/rag.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index d01d6e12..9368f558 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -251,15 +251,15 @@ def rag_draw(labels, rag, img, border_color=(0, 0, 0), node_color = (1, 1, 0), ---------- labels : ndarray, shape(M, N, [..., P,]) The labelled image. This should have one dimension less than - `img`. If `image` has dimensions `(M, N, 3)` `labels` should have + `img`. If `img` has dimensions `(M, N, 3)` `labels` should have dimensions `(M, N)`. rag : RAG The Region Adjacency Graph. img : ndarray, shape(M, N, [..., P,] 3) Input image. border_color : length-3 sequence, optional - RGB color of the corder of regions. Specifying `None` won't draw - the border. + RGB color of the border of regions. Specifying `None` won't draw + the border. Black by default. node_color : length-3 sequeunce, optional RGB color of the centroid of nodes. Yellow by default. low_color : length-3 sequeunce, optional From 78caebf6d25b6bbba070b0d3b74abc26800dfa36 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Tue, 29 Jul 2014 03:31:12 +0530 Subject: [PATCH 0255/1122] string wrap --- doc/examples/plot_rag_draw.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/examples/plot_rag_draw.py b/doc/examples/plot_rag_draw.py index 1c47743e..51a33dbd 100644 --- a/doc/examples/plot_rag_draw.py +++ b/doc/examples/plot_rag_draw.py @@ -21,8 +21,8 @@ plt.imshow(out) out = graph.rag_draw(labels, g, img, high_color=(1, 0, 0), thresh=30) plt.figure() -plt.title("RAG with edge weights less than 30,\ - color mapped between green and red.") +plt.title("RAG with edge weights less than 30, color " + "mapped between green and red.") plt.imshow(out) plt.show() From 4005749a61f6022e3adc509b775f9d4c8a5a2c2d Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Tue, 5 Aug 2014 23:05:15 +0530 Subject: [PATCH 0256/1122] rebase and change API to support mpl colorspec --- doc/examples/plot_rag_draw.py | 13 ++--- skimage/graph/__init__.py | 2 + skimage/graph/rag.py | 89 ++++++++++++++++++----------------- 3 files changed, 56 insertions(+), 48 deletions(-) diff --git a/doc/examples/plot_rag_draw.py b/doc/examples/plot_rag_draw.py index 51a33dbd..21dbbc28 100644 --- a/doc/examples/plot_rag_draw.py +++ b/doc/examples/plot_rag_draw.py @@ -1,25 +1,26 @@ """ -=========== -RAG Drawing -=========== +===================================== +Drawing Region Adjacency Graphs (RAGs) +====================================== This example constructs a Region Adjacency Graph (RAG) and draws it with the `rag_draw` method. """ from skimage import graph, data, segmentation -from matplotlib import pyplot as plt +from matplotlib import pyplot as plt, colors img = data.coffee() labels = segmentation.slic(img, compactness=30, n_segments=400) g = graph.rag_mean_color(img, labels) -out = graph.rag_draw(labels, g, img) +out = graph.draw_rag(labels, g, img) plt.figure() plt.title("RAG with all edges shown in green.") plt.imshow(out) -out = graph.rag_draw(labels, g, img, high_color=(1, 0, 0), thresh=30) +cmap = colors.ListedColormap(['cyan', 'red']) +out = graph.draw_rag(labels, g, img, colormap=cmap, thresh=30) plt.figure() plt.title("RAG with edge weights less than 30, color " "mapped between green and red.") diff --git a/skimage/graph/__init__.py b/skimage/graph/__init__.py index 2f1162d3..2cd422ba 100644 --- a/skimage/graph/__init__.py +++ b/skimage/graph/__init__.py @@ -3,6 +3,7 @@ from .mcp import MCP, MCP_Geometric, MCP_Connect, MCP_Flexible, route_through_ar from .rag import rag_mean_color, RAG from .graph_cut import cut_threshold, cut_normalized from .rag import rag_mean_color, RAG, rag_draw +from .rag import rag_mean_color, RAG, draw_rag from .graph_cut import cut_threshold ncut = cut_normalized @@ -18,4 +19,5 @@ __all__ = ['shortest_path', 'cut_normalized', 'ncut', 'rag_draw', + 'draw_rag', 'RAG'] diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 9368f558..4447c4e3 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -9,11 +9,21 @@ except ImportError: raise ImportError(msg) import warnings warnings.warn(msg) + import numpy as np from scipy.ndimage import filters from scipy import ndimage as nd +<<<<<<< HEAD import math from .. import draw, measure, segmentation +======= +from .. import draw +from .. import measure +from .. import segmentation +from matplotlib import colors +from matplotlib import cm +from .. import util +>>>>>>> rebase and change API to support mpl colorspec def min_weight(graph, src, dst, n): @@ -239,8 +249,8 @@ def rag_mean_color(image, labels, connectivity=2, mode='distance', return graph -def rag_draw(labels, rag, img, border_color=(0, 0, 0), node_color = (1, 1, 0), - low_color = (0, 1, 0), high_color=None, thresh=np.inf): +def draw_rag(labels, rag, img, border_color=None, node_color='yellow', + edge_color='green', colormap=None, thresh=np.inf): """Draw a Region Adjacency Graph on an image. Given a labelled image and its corresponding RAG, draw the nodes and edges @@ -249,30 +259,24 @@ def rag_draw(labels, rag, img, border_color=(0, 0, 0), node_color = (1, 1, 0), Parameters ---------- - labels : ndarray, shape(M, N, [..., P,]) - The labelled image. This should have one dimension less than - `img`. If `img` has dimensions `(M, N, 3)` `labels` should have - dimensions `(M, N)`. + labels : ndarray, shape(M, N) + The labelled image. rag : RAG The Region Adjacency Graph. - img : ndarray, shape(M, N, [..., P,] 3) + img : ndarray, shape(M, N, 3) Input image. - border_color : length-3 sequence, optional - RGB color of the border of regions. Specifying `None` won't draw - the border. Black by default. - node_color : length-3 sequeunce, optional - RGB color of the centroid of nodes. Yellow by default. - low_color : length-3 sequeunce, optional - RGB color of the edges. If `high_color` is not specified, all edges - are draw with `low_color`. Green by default. - high_color : length-3 sequeunce, optional - RGB color of the edges with high weight. If specified, the edges are - color mapped between `low_color` and `high_color` depending on their - weight. Edges with low weights are more like `low_color` whereas edges - with high weights are more like `high_color`. - thresh : float, optiona; + border_color : colorspec, optional + Any matplotlib colorspec. + node_color : colorspec, optional + Any matplotlib colorspec. Yellow by default. + edge_color : colorspec, optional + Any matplotlib colorspec. Green by default. + colormap : colormap, optional + Any matplotlib colormap. If specified the edges are colormapped with + the specified color map. + thresh : float, optional Edges with weight below `thresh` are not drawn, or considered for color - mapping in case `high_color` is specified. + mapping. Returns ------- @@ -282,41 +286,44 @@ def rag_draw(labels, rag, img, border_color=(0, 0, 0), node_color = (1, 1, 0), Examples -------- >>> from skimage import data, graph, segmentation - >>> img = data.lena() + >>> img = data.coffee() >>> labels = segmentation.slic(img) >>> g = graph.rag_mean_color(img, labels) >>> out = graph.rag_draw(labels, g, img) """ rag = rag.copy() - rag_labels = labels.copy() - out = img.copy() + out = util.img_as_float(img) + cc = colors.ColorConverter() - low_color = np.array(low_color) - - if not high_color is None: - high_color = np.array(high_color) + edge_color = cc.to_rgb(edge_color) + node_color = cc.to_rgb(node_color) # Handling the case where one node has multiple labels # offset is 1 so that regionprops does not ignore 0 offset = 1 + map_array = np.arange(labels.max() + 1) for n, d in rag.nodes_iter(data=True): - for l in d['labels']: - rag_labels[labels == l] = offset + for label in d['labels']: + map_array[label] = offset offset += 1 + rag_labels = map_array[labels] + regions = measure.regionprops(rag_labels) for region in regions: # Because we kept the offset as 1 rag.node[region['label'] - 1]['centroid'] = region['centroid'] if not border_color is None: + border_color = cc.to_rgb(border_color) out = segmentation.mark_boundaries(out, rag_labels, color=border_color) - if not high_color is None: - max_weight = max([d['weight'] for x, y, d in rag.edges_iter(data=True) - if d['weight'] < thresh]) - min_weight = min([d['weight'] for x, y, d in rag.edges_iter(data=True) - if d['weight'] < thresh]) + if colormap is not None: + edge_weight_list = [d['weight'] for x, y, d in + rag.edges_iter(data=True) if d['weight'] < thresh] + norm = colors.Normalize() + norm.autoscale(edge_weight_list) + smap = cm.ScalarMappable(norm, colormap) for n1, n2, data in rag.edges_iter(data=True): @@ -327,13 +334,11 @@ def rag_draw(labels, rag, img, border_color=(0, 0, 0), node_color = (1, 1, 0), line = draw.line(r1, c1, r2, c2) - if not high_color is None: - norm_weight = ((rag[n1][n2]['weight'] - min_weight) / - (max_weight - min_weight)) - out[line] = (norm_weight * high_color + - (1 - norm_weight) * low_color) + if colormap is not None: + current_color = smap.to_rgba([data['weight']])[0][:-1] + out[line] = current_color else: - out[line] = low_color + out[line] = edge_color circle = draw.circle(r1, c1, 2) out[circle] = node_color From 95b20adee7ab33880163ffa81d51bd7f836a33e0 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Tue, 5 Aug 2014 23:07:39 +0530 Subject: [PATCH 0257/1122] None comparison --- skimage/graph/rag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 4447c4e3..f3c22f83 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -314,7 +314,7 @@ def draw_rag(labels, rag, img, border_color=None, node_color='yellow', # Because we kept the offset as 1 rag.node[region['label'] - 1]['centroid'] = region['centroid'] - if not border_color is None: + if border_color is not None: border_color = cc.to_rgb(border_color) out = segmentation.mark_boundaries(out, rag_labels, color=border_color) From adeb8689afaeea1852cb3d0dd9ccf76c0ccb509a Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Tue, 5 Aug 2014 23:43:07 +0530 Subject: [PATCH 0258/1122] Fixed doctest --- skimage/graph/rag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index f3c22f83..7e87a784 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -289,7 +289,7 @@ def draw_rag(labels, rag, img, border_color=None, node_color='yellow', >>> img = data.coffee() >>> labels = segmentation.slic(img) >>> g = graph.rag_mean_color(img, labels) - >>> out = graph.rag_draw(labels, g, img) + >>> out = graph.draw_rag(labels, g, img) """ rag = rag.copy() out = util.img_as_float(img) From 5bd55071bc28a9ff402e966b976ef7d379c4e89d Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Wed, 6 Aug 2014 00:32:47 +0530 Subject: [PATCH 0259/1122] handled matplotlib not present case --- skimage/graph/rag.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 7e87a784..e3849184 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -13,17 +13,14 @@ except ImportError: import numpy as np from scipy.ndimage import filters from scipy import ndimage as nd -<<<<<<< HEAD import math -from .. import draw, measure, segmentation -======= -from .. import draw -from .. import measure -from .. import segmentation -from matplotlib import colors -from matplotlib import cm -from .. import util ->>>>>>> rebase and change API to support mpl colorspec +from .. import draw, measure, segmentation, util +try: + from matplotlib import colors + from matplotlib import cm +except ImportError: + pass + def min_weight(graph, src, dst, n): From bcbfdb28f59170c2c96aea9444c093e9556f6df2 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 14 Aug 2014 01:20:55 +0530 Subject: [PATCH 0260/1122] add desaturate option --- doc/examples/plot_rag_draw.py | 4 ++-- skimage/graph/rag.py | 38 ++++++++++++++++++++++------------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/doc/examples/plot_rag_draw.py b/doc/examples/plot_rag_draw.py index 21dbbc28..61d7e134 100644 --- a/doc/examples/plot_rag_draw.py +++ b/doc/examples/plot_rag_draw.py @@ -19,8 +19,8 @@ plt.figure() plt.title("RAG with all edges shown in green.") plt.imshow(out) -cmap = colors.ListedColormap(['cyan', 'red']) -out = graph.draw_rag(labels, g, img, colormap=cmap, thresh=30) +cmap = colors.ListedColormap(['#00ff00', '#ff0000']) +out = graph.draw_rag(labels, g, img,colormap=cmap, thresh=30, desaturate=True) plt.figure() plt.title("RAG with edge weights less than 30, color " "mapped between green and red.") diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index e3849184..e1ab79ca 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -14,7 +14,7 @@ import numpy as np from scipy.ndimage import filters from scipy import ndimage as nd import math -from .. import draw, measure, segmentation, util +from .. import draw, measure, segmentation, util, color try: from matplotlib import colors from matplotlib import cm @@ -246,8 +246,9 @@ def rag_mean_color(image, labels, connectivity=2, mode='distance', return graph -def draw_rag(labels, rag, img, border_color=None, node_color='yellow', - edge_color='green', colormap=None, thresh=np.inf): +def draw_rag(labels, rag, img, border_color=None, node_color='#ffff00', + edge_color='#00ff00', colormap=None, thresh=np.inf, + desaturate=False, in_place=True): """Draw a Region Adjacency Graph on an image. Given a labelled image and its corresponding RAG, draw the nodes and edges @@ -256,11 +257,11 @@ def draw_rag(labels, rag, img, border_color=None, node_color='yellow', Parameters ---------- - labels : ndarray, shape(M, N) + labels : ndarray, shape (M, N) The labelled image. rag : RAG The Region Adjacency Graph. - img : ndarray, shape(M, N, 3) + img : ndarray, shape (M, N, 3) Input image. border_color : colorspec, optional Any matplotlib colorspec. @@ -274,10 +275,16 @@ def draw_rag(labels, rag, img, border_color=None, node_color='yellow', thresh : float, optional Edges with weight below `thresh` are not drawn, or considered for color mapping. + desaturate : bool, optional + Convert the image to grayscale before displaying. Particularly helps + visualiztion when using the `colormap` option. + in_place : bool, optional + If set, the RAG is modified in place. For each node `n` the function + will set a new attribute ``rag.node[n]['centroid']``. Returns ------- - out : ndarray, shape(M, N, [..., P,] 3) + out : ndarray, shape (M, N, 3) The image with the RAG drawn. Examples @@ -288,7 +295,13 @@ def draw_rag(labels, rag, img, border_color=None, node_color='yellow', >>> g = graph.rag_mean_color(img, labels) >>> out = graph.draw_rag(labels, g, img) """ - rag = rag.copy() + if not in_place: + rag = rag.copy() + + if desaturate: + img = color.rgb2gray(img) + img = color.gray2rgb(img) + out = util.img_as_float(img) cc = colors.ColorConverter() @@ -305,11 +318,10 @@ def draw_rag(labels, rag, img, border_color=None, node_color='yellow', offset += 1 rag_labels = map_array[labels] - regions = measure.regionprops(rag_labels) - for region in regions: - # Because we kept the offset as 1 - rag.node[region['label'] - 1]['centroid'] = region['centroid'] + + for (n, data), region in zip(rag.nodes_iter(data=True), regions): + data['centroid'] = region['centroid'] if border_color is not None: border_color = cc.to_rgb(border_color) @@ -328,12 +340,10 @@ def draw_rag(labels, rag, img, border_color=None, node_color='yellow', continue r1, c1 = map(int, rag.node[n1]['centroid']) r2, c2 = map(int, rag.node[n2]['centroid']) - line = draw.line(r1, c1, r2, c2) if colormap is not None: - current_color = smap.to_rgba([data['weight']])[0][:-1] - out[line] = current_color + out[line] = smap.to_rgba([data['weight']])[0][:-1] else: out[line] = edge_color From 3553809c55cb9042f197cfee1fdd89b8fafc5da3 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 14 Aug 2014 01:24:02 +0530 Subject: [PATCH 0261/1122] pep8 --- doc/examples/plot_rag_draw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/examples/plot_rag_draw.py b/doc/examples/plot_rag_draw.py index 61d7e134..9c9d68b0 100644 --- a/doc/examples/plot_rag_draw.py +++ b/doc/examples/plot_rag_draw.py @@ -20,7 +20,7 @@ plt.title("RAG with all edges shown in green.") plt.imshow(out) cmap = colors.ListedColormap(['#00ff00', '#ff0000']) -out = graph.draw_rag(labels, g, img,colormap=cmap, thresh=30, desaturate=True) +out = graph.draw_rag(labels, g, img, colormap=cmap, thresh=30, desaturate=True) plt.figure() plt.title("RAG with edge weights less than 30, color " "mapped between green and red.") From 5c5b60df4cde95d1115c8c303ca9f48fe708f169 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Fri, 15 Aug 2014 02:32:23 +0530 Subject: [PATCH 0262/1122] Chnaged palette in example --- doc/examples/plot_rag_draw.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/examples/plot_rag_draw.py b/doc/examples/plot_rag_draw.py index 9c9d68b0..bb8042d8 100644 --- a/doc/examples/plot_rag_draw.py +++ b/doc/examples/plot_rag_draw.py @@ -19,8 +19,11 @@ plt.figure() plt.title("RAG with all edges shown in green.") plt.imshow(out) -cmap = colors.ListedColormap(['#00ff00', '#ff0000']) -out = graph.draw_rag(labels, g, img, colormap=cmap, thresh=30, desaturate=True) +# The color palette used was taken from +# http://www.colorcombos.com/color-schemes/2/ColorCombo2.html +cmap = colors.ListedColormap(['#6599FF', '#ff9900']) +out = graph.draw_rag(labels, g, img, node_color="#ffde00", colormap=cmap, + thresh=30, desaturate=True) plt.figure() plt.title("RAG with edge weights less than 30, color " "mapped between green and red.") From cfaa83cf26f3d12f0454b1a62e41fabc0e76a63b Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Fri, 15 Aug 2014 02:33:09 +0530 Subject: [PATCH 0263/1122] Chnaged palette in example --- doc/examples/plot_rag_draw.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/examples/plot_rag_draw.py b/doc/examples/plot_rag_draw.py index bb8042d8..49bdf42c 100644 --- a/doc/examples/plot_rag_draw.py +++ b/doc/examples/plot_rag_draw.py @@ -18,6 +18,7 @@ out = graph.draw_rag(labels, g, img) plt.figure() plt.title("RAG with all edges shown in green.") plt.imshow(out) +io.imsave("/home/vighnesh/Desktop/1.png") # The color palette used was taken from # http://www.colorcombos.com/color-schemes/2/ColorCombo2.html @@ -28,5 +29,6 @@ plt.figure() plt.title("RAG with edge weights less than 30, color " "mapped between green and red.") +io.imsave("/home/vighnesh/Desktop/2.png") plt.imshow(out) plt.show() From 82f158482e38e519cdb9af776e7937c23dbbe67f Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Fri, 15 Aug 2014 02:35:28 +0530 Subject: [PATCH 0264/1122] removed file save --- doc/examples/plot_rag_draw.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/examples/plot_rag_draw.py b/doc/examples/plot_rag_draw.py index 49bdf42c..bb8042d8 100644 --- a/doc/examples/plot_rag_draw.py +++ b/doc/examples/plot_rag_draw.py @@ -18,7 +18,6 @@ out = graph.draw_rag(labels, g, img) plt.figure() plt.title("RAG with all edges shown in green.") plt.imshow(out) -io.imsave("/home/vighnesh/Desktop/1.png") # The color palette used was taken from # http://www.colorcombos.com/color-schemes/2/ColorCombo2.html @@ -29,6 +28,5 @@ plt.figure() plt.title("RAG with edge weights less than 30, color " "mapped between green and red.") -io.imsave("/home/vighnesh/Desktop/2.png") plt.imshow(out) plt.show() From e42c0652434535459d3f4c903d7dd324091c8090 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Fri, 15 Aug 2014 02:37:59 +0530 Subject: [PATCH 0265/1122] corrected title --- doc/examples/plot_rag_draw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/examples/plot_rag_draw.py b/doc/examples/plot_rag_draw.py index bb8042d8..2572a3fb 100644 --- a/doc/examples/plot_rag_draw.py +++ b/doc/examples/plot_rag_draw.py @@ -26,7 +26,7 @@ out = graph.draw_rag(labels, g, img, node_color="#ffde00", colormap=cmap, thresh=30, desaturate=True) plt.figure() plt.title("RAG with edge weights less than 30, color " - "mapped between green and red.") + "mapped between blue and orange.") plt.imshow(out) plt.show() From 7730eb004e68ffe382cf3a57ae0ca26f4ec618ef Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Fri, 15 Aug 2014 14:13:36 +0530 Subject: [PATCH 0266/1122] fixed init --- skimage/graph/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/skimage/graph/__init__.py b/skimage/graph/__init__.py index 2cd422ba..29bcb031 100644 --- a/skimage/graph/__init__.py +++ b/skimage/graph/__init__.py @@ -2,7 +2,6 @@ from .spath import shortest_path from .mcp import MCP, MCP_Geometric, MCP_Connect, MCP_Flexible, route_through_array from .rag import rag_mean_color, RAG from .graph_cut import cut_threshold, cut_normalized -from .rag import rag_mean_color, RAG, rag_draw from .rag import rag_mean_color, RAG, draw_rag from .graph_cut import cut_threshold ncut = cut_normalized @@ -18,6 +17,5 @@ __all__ = ['shortest_path', 'cut_threshold', 'cut_normalized', 'ncut', - 'rag_draw', 'draw_rag', 'RAG'] From 4bb4076959b0c6e9f6986dbc7ef4381eb7bc21de Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Fri, 15 Aug 2014 14:15:02 +0530 Subject: [PATCH 0267/1122] fixed imports --- skimage/graph/__init__.py | 2 -- skimage/graph/rag.py | 1 - 2 files changed, 3 deletions(-) diff --git a/skimage/graph/__init__.py b/skimage/graph/__init__.py index 29bcb031..6da4fcc8 100644 --- a/skimage/graph/__init__.py +++ b/skimage/graph/__init__.py @@ -1,9 +1,7 @@ from .spath import shortest_path from .mcp import MCP, MCP_Geometric, MCP_Connect, MCP_Flexible, route_through_array -from .rag import rag_mean_color, RAG from .graph_cut import cut_threshold, cut_normalized from .rag import rag_mean_color, RAG, draw_rag -from .graph_cut import cut_threshold ncut = cut_normalized diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index e1ab79ca..74e4ab07 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -22,7 +22,6 @@ except ImportError: pass - def min_weight(graph, src, dst, n): """Callback to handle merging nodes by choosing minimum weight. From 6e09628fb909110bf1b79f333a411b4a02a5b57f Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Fri, 15 Aug 2014 14:37:14 +0530 Subject: [PATCH 0268/1122] update relase doc --- doc/release/release_dev.txt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/release/release_dev.txt b/doc/release/release_dev.txt index 1123eb77..cbeded27 100644 --- a/doc/release/release_dev.txt +++ b/doc/release/release_dev.txt @@ -14,9 +14,12 @@ http://scikit-image.org New Features ------------ - - - +Region Adjacency Graphs + - Color distance RAGs (#1031) + - Threshold Cut on RAGs (#1031) + - Similarity RAGs (#1080) + - Normalized Cut on RAGs (#1080) + - RAG Drawing (#1087) Improvements ------------ From c43180fa27b0add7a0ad71d9e159efdd156fac06 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Fri, 15 Aug 2014 17:02:35 +0530 Subject: [PATCH 0269/1122] force copy --- skimage/graph/rag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 74e4ab07..edd9ecde 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -301,7 +301,7 @@ def draw_rag(labels, rag, img, border_color=None, node_color='#ffff00', img = color.rgb2gray(img) img = color.gray2rgb(img) - out = util.img_as_float(img) + out = util.img_as_float(img, force_copy=True) cc = colors.ColorConverter() edge_color = cc.to_rgb(edge_color) From 344e2a51c0cd4ec79843805393514402f1a18df9 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Fri, 15 Aug 2014 17:52:25 +0530 Subject: [PATCH 0270/1122] cast labels in ncut --- skimage/graph/graph_cut.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 3f973e22..10ea77c4 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -120,7 +120,7 @@ def cut_normalized(labels, rag, thresh=0.001, num_cuts=10, in_place=True): _ncut_relabel(rag, thresh, num_cuts) - map_array = np.zeros(labels.max() + 1) + map_array = np.zeros(labels.max() + 1, dtype=labels.dtype) # Mapping from old labels to new for n, d in rag.nodes_iter(data=True): map_array[d['labels']] = d['ncut label'] From 2b1dde202f3d8a6b12a4516dfeadba7cbcbe61ff Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 15 Aug 2014 15:21:24 +0200 Subject: [PATCH 0271/1122] freeimage: Correctly handle saving uint16 images (closes gh-1101) --- skimage/io/_plugins/freeimage_plugin.py | 27 +++++++++++++++++-------- skimage/io/tests/test_freeimage.py | 6 +++--- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/skimage/io/_plugins/freeimage_plugin.py b/skimage/io/_plugins/freeimage_plugin.py index 5ebe8214..c3bd4e73 100644 --- a/skimage/io/_plugins/freeimage_plugin.py +++ b/skimage/io/_plugins/freeimage_plugin.py @@ -518,7 +518,6 @@ def _array_from_bitmap(bitmap): # swizzle the color components and flip the scanlines to go from # FreeImage's BGR[A] and upside-down internal memory format to something # more normal - def n(arr): return arr[..., ::-1].T if len(shape) == 3 and _FI.FreeImage_IsLittleEndian() and \ @@ -674,16 +673,28 @@ def _array_to_bitmap(array): try: def n(arr): # normalise to freeimage's in-memory format return arr.T[:, ::-1] + wrapped_array = _wrap_bitmap_bits_in_array(bitmap, w_shape, dtype) # swizzle the color components and flip the scanlines to go to # FreeImage's BGR[A] and upside-down internal memory format - if len(shape) == 3 and _FI.FreeImage_IsLittleEndian() and \ - dtype.type == numpy.uint8: - wrapped_array[0] = n(array[:, :, 2]) - wrapped_array[1] = n(array[:, :, 1]) - wrapped_array[2] = n(array[:, :, 0]) - if shape[2] == 4: - wrapped_array[3] = n(array[:, :, 3]) + if len(shape) == 3: + R = array[:, :, 0] + G = array[:, :, 1] + B = array[:, :, 2] + + if _FI.FreeImage_IsLittleEndian(): + if dtype.type == numpy.uint8: + wrapped_array[0] = n(B) + wrapped_array[1] = n(G) + wrapped_array[2] = n(R) + elif dtype.type == numpy.uint16: + wrapped_array[0] = n(R) + wrapped_array[1] = n(G) + wrapped_array[2] = n(B) + + if shape[2] == 4: + A = array[:, :, 3] + wrapped_array[3] = n(A) else: wrapped_array[:] = n(array) if len(shape) == 2 and dtype.type == numpy.uint8: diff --git a/skimage/io/tests/test_freeimage.py b/skimage/io/tests/test_freeimage.py index a2d88db3..81f8058a 100644 --- a/skimage/io/tests/test_freeimage.py +++ b/skimage/io/tests/test_freeimage.py @@ -81,15 +81,15 @@ class TestSave: f.close() sio.imsave(fname, x) y = sio.imread(fname) - assert_array_equal(x, y) + assert_array_equal(y, x) @skipif(not FI_available) def test_imsave_roundtrip(self): for shape, dtype, format in [ [(10, 10), (np.uint8, np.uint16), ('tif', 'png')], [(10, 10), (np.float32,), ('tif',)], - [(10, 10, 3), (np.uint8,), ('png',)], - [(10, 10, 4), (np.uint8,), ('png',)] + [(10, 10, 3), (np.uint8, np.uint16), ('png',)], + [(10, 10, 4), (np.uint8, np.uint16), ('png',)] ]: tests = [(d, f) for d in dtype for f in format] for d, f in tests: From 3f7ce2e670b1be9c9f79ed8c7b51172c682c46b1 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Fri, 15 Aug 2014 21:28:47 +0530 Subject: [PATCH 0272/1122] Added example with cubehelix --- doc/examples/plot_rag_draw.py | 7 +++++++ skimage/graph/rag.py | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/doc/examples/plot_rag_draw.py b/doc/examples/plot_rag_draw.py index 2572a3fb..abf2ba08 100644 --- a/doc/examples/plot_rag_draw.py +++ b/doc/examples/plot_rag_draw.py @@ -27,6 +27,13 @@ out = graph.draw_rag(labels, g, img, node_color="#ffde00", colormap=cmap, plt.figure() plt.title("RAG with edge weights less than 30, color " "mapped between blue and orange.") +plt.imshow(out) + +plt.figure() +plt.title("All edges drawn with cubehelix colormap") +cmap = plt.get_cmap('cubehelix') +out = graph.draw_rag(labels, g, img, colormap=cmap, + desaturate=True) plt.imshow(out) plt.show() diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index edd9ecde..edaec4b3 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -251,8 +251,8 @@ def draw_rag(labels, rag, img, border_color=None, node_color='#ffff00', """Draw a Region Adjacency Graph on an image. Given a labelled image and its corresponding RAG, draw the nodes and edges - of the RAG on the image with the specified colors. Nodes are markes by - the centroids of the corresposning regions. + of the RAG on the image with the specified colors. Nodes are marked by + the centroids of the corresponding regions. Parameters ---------- @@ -276,7 +276,7 @@ def draw_rag(labels, rag, img, border_color=None, node_color='#ffff00', mapping. desaturate : bool, optional Convert the image to grayscale before displaying. Particularly helps - visualiztion when using the `colormap` option. + visualization when using the `colormap` option. in_place : bool, optional If set, the RAG is modified in place. For each node `n` the function will set a new attribute ``rag.node[n]['centroid']``. From a52cb01a49038870d7e0f91a9ddbcf0b027da153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20M=C3=A1rquez=20Neila?= Date: Sun, 17 Aug 2014 22:09:03 +0200 Subject: [PATCH 0273/1122] Fix bugs in HOG --- skimage/feature/_hog.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/skimage/feature/_hog.py b/skimage/feature/_hog.py index 67922b92..da9f737e 100644 --- a/skimage/feature/_hog.py +++ b/skimage/feature/_hog.py @@ -1,6 +1,6 @@ import numpy as np from scipy import sqrt, pi, arctan2, cos, sin -from scipy.ndimage import uniform_filter +from scipy.ndimage import uniform_filter, convolve1d def hog(image, orientations=9, pixels_per_cell=(8, 8), @@ -80,10 +80,8 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), # to avoid problems with subtracting unsigned numbers in np.diff() image = image.astype('float') - gx = np.zeros(image.shape) - gy = np.zeros(image.shape) - gx[:, :-1] = np.diff(image, n=1, axis=1) - gy[:-1, :] = np.diff(image, n=1, axis=0) + gx = convolve1d(image, [-1,0,1], axis=1, mode='nearest') + gy = convolve1d(image, [-1,0,1], axis=0, mode='nearest') """ The third stage aims to produce an encoding that is sensitive to @@ -144,9 +142,9 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), dx = radius * cos(float(o) / orientations * np.pi) dy = radius * sin(float(o) / orientations * np.pi) rr, cc = draw.line(int(centre[0] - dx), - int(centre[1] - dy), + int(centre[1] + dy), int(centre[0] + dx), - int(centre[1] + dy)) + int(centre[1] - dy)) hog_image[rr, cc] += orientation_histogram[y, x, o] """ From 23bc3ed077b8594331047aa975b5b37db2a366de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20M=C3=A1rquez=20Neila?= Date: Mon, 18 Aug 2014 01:34:54 +0200 Subject: [PATCH 0274/1122] Replaced convolve1d by ad-hoc gradient computation --- skimage/feature/_hog.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/skimage/feature/_hog.py b/skimage/feature/_hog.py index da9f737e..f9608c9d 100644 --- a/skimage/feature/_hog.py +++ b/skimage/feature/_hog.py @@ -1,6 +1,6 @@ import numpy as np from scipy import sqrt, pi, arctan2, cos, sin -from scipy.ndimage import uniform_filter, convolve1d +from scipy.ndimage import uniform_filter def hog(image, orientations=9, pixels_per_cell=(8, 8), @@ -79,9 +79,15 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), # convert uint image to float # to avoid problems with subtracting unsigned numbers in np.diff() image = image.astype('float') - - gx = convolve1d(image, [-1,0,1], axis=1, mode='nearest') - gy = convolve1d(image, [-1,0,1], axis=0, mode='nearest') + + gx = np.empty_like(image) + gx[:, 0] = 0 + gx[:, -1] = 0 + gx[:, 1:-1] = image[:, 2:] - image[:, :-2] + gy = np.empty_like(image) + gy[0, :] = 0 + gy[-1, :] = 0 + gy[1:-1, :] = image[2:, :] - image[:-2, :] """ The third stage aims to produce an encoding that is sensitive to From 808e53c7ae53ad80e1c8f8ed77b34f1b38b569df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20M=C3=A1rquez=20Neila?= Date: Mon, 18 Aug 2014 02:12:27 +0200 Subject: [PATCH 0275/1122] Code valid for all image dtypes --- skimage/feature/_hog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/feature/_hog.py b/skimage/feature/_hog.py index f9608c9d..2c6dbf0a 100644 --- a/skimage/feature/_hog.py +++ b/skimage/feature/_hog.py @@ -80,11 +80,11 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), # to avoid problems with subtracting unsigned numbers in np.diff() image = image.astype('float') - gx = np.empty_like(image) + gx = np.empty(image.shape) gx[:, 0] = 0 gx[:, -1] = 0 gx[:, 1:-1] = image[:, 2:] - image[:, :-2] - gy = np.empty_like(image) + gy = np.empty(image.shape) gy[0, :] = 0 gy[-1, :] = 0 gy[1:-1, :] = image[2:, :] - image[:-2, :] From 64e20d817344040ec7861c269d090cb2b3bd9b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20M=C3=A1rquez=20Neila?= Date: Mon, 18 Aug 2014 02:47:29 +0200 Subject: [PATCH 0276/1122] Minor fix --- skimage/feature/_hog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/feature/_hog.py b/skimage/feature/_hog.py index 2c6dbf0a..72411308 100644 --- a/skimage/feature/_hog.py +++ b/skimage/feature/_hog.py @@ -80,11 +80,11 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), # to avoid problems with subtracting unsigned numbers in np.diff() image = image.astype('float') - gx = np.empty(image.shape) + gx = np.empty(image.shape, dtype=np.double) gx[:, 0] = 0 gx[:, -1] = 0 gx[:, 1:-1] = image[:, 2:] - image[:, :-2] - gy = np.empty(image.shape) + gy = np.empty(image.shape, dtype=np.double) gy[0, :] = 0 gy[-1, :] = 0 gy[1:-1, :] = image[2:, :] - image[:-2, :] From 12b1a91c708de85f64938b2a5f40ff195653ced3 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 18 Aug 2014 18:42:23 +0530 Subject: [PATCH 0277/1122] remove self loops in rag --- skimage/graph/graph_cut.py | 10 +++++++++- skimage/graph/rag.py | 3 ++- skimage/graph/tests/test_rag.py | 2 ++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 3f973e22..284b3c61 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -72,7 +72,8 @@ def cut_threshold(labels, rag, thresh, in_place=True): return map_array[labels] -def cut_normalized(labels, rag, thresh=0.001, num_cuts=10, in_place=True): +def cut_normalized(labels, rag, thresh=0.001, num_cuts=10, in_place=True, + max_edge=1.0): """Perform Normalized Graph cut on the Region Adjacency Graph. Given an image's labels and its similarity RAG, recursively perform @@ -94,6 +95,10 @@ def cut_normalized(labels, rag, thresh=0.001, num_cuts=10, in_place=True): in_place : bool If set, modifies `rag` in place. For each node `n` the function will set a new attribute ``rag.node[n]['ncut label']``. + max_egde : float, optinal + The maximum possible value of an edge in the RAG. This corresponds to + an edge between regions which are identical. This is used to put self + edges in the RAG. Returns ------- @@ -118,6 +123,9 @@ def cut_normalized(labels, rag, thresh=0.001, num_cuts=10, in_place=True): if not in_place: rag = rag.copy() + for node in rag.nodes_iter(): + rag.add_edge(node, node, weight=max_edge) + _ncut_relabel(rag, thresh, num_cuts) map_array = np.zeros(labels.max() + 1) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 5ef9a83b..df9b4ebf 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -115,7 +115,8 @@ def _add_edge_filter(values, graph): values = values.astype(int) current = values[0] for value in values[1:]: - graph.add_edge(current, value) + if value != current: + graph.add_edge(current, value) return 0 diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 0e62aa1a..26f5a85c 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -104,5 +104,7 @@ def test_cut_normalized(): def test_rag_error(): img = np.zeros((10, 10, 3), dtype='uint8') labels = np.zeros((10, 10), dtype='uint8') + labels[:5, :] = 0 + labels[5:, :] = 1 testing.assert_raises(ValueError, graph.rag_mean_color, img, labels, 2, 'non existant mode') From 103f48bc0091d969ce209e14a56e6b21dddb00c7 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Mon, 18 Aug 2014 19:58:02 +0530 Subject: [PATCH 0278/1122] Spelling error --- skimage/graph/graph_cut.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/graph/graph_cut.py b/skimage/graph/graph_cut.py index 284b3c61..d7710f0b 100644 --- a/skimage/graph/graph_cut.py +++ b/skimage/graph/graph_cut.py @@ -95,9 +95,9 @@ def cut_normalized(labels, rag, thresh=0.001, num_cuts=10, in_place=True, in_place : bool If set, modifies `rag` in place. For each node `n` the function will set a new attribute ``rag.node[n]['ncut label']``. - max_egde : float, optinal + max_edge : float, optional The maximum possible value of an edge in the RAG. This corresponds to - an edge between regions which are identical. This is used to put self + an edge between identical regions. This is used to put self edges in the RAG. Returns From 26e0348f3cf76d68f9afc043c7928bc25152a0e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Mon, 25 Aug 2014 08:55:53 -0400 Subject: [PATCH 0279/1122] Comments of the PR --- doc/source/user_guide/parallelization.txt | 25 +++++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/doc/source/user_guide/parallelization.txt b/doc/source/user_guide/parallelization.txt index d4ffe59b..20517de5 100755 --- a/doc/source/user_guide/parallelization.txt +++ b/doc/source/user_guide/parallelization.txt @@ -11,14 +11,15 @@ on a large batch of images. Let us define an example. from skimage.restoration import denoise_tv_chambolle from skimage.feature import hog - def tasks(image): + def task(image): """ - Apply some functions. + Apply some functions and return an image. """ image = denoise_tv_chambolle(image, weight=0.1, multichannel=True) fd, hog_image = hog(color.rgb2gray(image), orientations=8, pixels_per_cell=(16, 16), cells_per_block=(1, 1), visualise=True) + return hog_image # Prepare images @@ -26,7 +27,7 @@ on a large batch of images. Let us define an example. width = 10 pics = [hubble[:,slice:slice+width] for slice in range(0, 1000, width)] -To call the function ``tasks`` on each element of the list ``pics``, it is +To call the function ``task`` on each element of the list ``pics``, it is usual to write a for loop. To measure the execution time of this loop, a function is defined and called with ``timeit``. @@ -34,19 +35,25 @@ is defined and called with ``timeit``. def classic_loop(): for image in pics: - tasks(image) + task(image) import timeit - print("classic_loop():", timeit.timeit("classic_loop()", setup="from __main__ import (classic_loop, tasks, pics)", number=1)) + print("classic_loop():", timeit.timeit("classic_loop()", setup="from __main__ import (classic_loop, task, pics)", number=1)) + +Alternatively, you can use ipython and measure the execution time with ``%timeit``. + +.. code-block:: python + + %timeit classic_loop() Another equivalent way to code this loop is to use a comprehension list which has the same efficiency. .. code-block:: python def comprehension_loop(): - [tasks(image) for image in pics] + [task(image) for image in pics] - print("comprehension_loop():", timeit.timeit("comprehension_loop()", setup="from __main__ import (comprehension_loop, tasks, pics)", number=1)) + %timeit comprehension_loop() ``joblib`` is a library providing an easy way to parallelize for loops once we have a comprehension list. The number of jobs can be specified. @@ -55,6 +62,6 @@ The number of jobs can be specified. from joblib import Parallel, delayed def joblib_loop(): - Parallel(n_jobs=4)(delayed(tasks)(i) for i in pics) + Parallel(n_jobs=4)(delayed(task)(i) for i in pics) - print("joblib_loop():", timeit.timeit("joblib_loop()", setup="from __main__ import (joblib_loop, tasks, pics)", number=1)) + %timeit joblib_loop() From 8a337dbe465aba617242719712607a38a983db91 Mon Sep 17 00:00:00 2001 From: Julien Coste Date: Sun, 31 Aug 2014 10:55:12 +0100 Subject: [PATCH 0280/1122] Move canny from filter to feature --- skimage/feature/__init__.py | 4 +++- skimage/{filter => feature}/_canny.py | 0 skimage/feature/tests/__init__.py | 1 + skimage/{filter => feature}/tests/test_canny.py | 2 +- skimage/filter/__init__.py | 3 ++- skimage/viewer/plugins/canny.py | 2 +- 6 files changed, 8 insertions(+), 4 deletions(-) rename skimage/{filter => feature}/_canny.py (100%) create mode 100644 skimage/feature/tests/__init__.py rename skimage/{filter => feature}/tests/test_canny.py (99%) diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index c46fde01..90bb6e53 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -1,3 +1,4 @@ +from ._canny import canny from ._daisy import daisy from ._hog import hog from .texture import greycomatrix, greycoprops, local_binary_pattern @@ -17,7 +18,8 @@ from .util import plot_matches from .blob import blob_dog, blob_log, blob_doh -__all__ = ['daisy', +__all__ = ['canny' + 'daisy', 'hog', 'greycomatrix', 'greycoprops', diff --git a/skimage/filter/_canny.py b/skimage/feature/_canny.py similarity index 100% rename from skimage/filter/_canny.py rename to skimage/feature/_canny.py diff --git a/skimage/feature/tests/__init__.py b/skimage/feature/tests/__init__.py new file mode 100644 index 00000000..fc970954 --- /dev/null +++ b/skimage/feature/tests/__init__.py @@ -0,0 +1 @@ +__author__ = 'julien' diff --git a/skimage/filter/tests/test_canny.py b/skimage/feature/tests/test_canny.py similarity index 99% rename from skimage/filter/tests/test_canny.py rename to skimage/feature/tests/test_canny.py index 2c758edf..43db5037 100644 --- a/skimage/filter/tests/test_canny.py +++ b/skimage/feature/tests/test_canny.py @@ -1,7 +1,7 @@ import unittest import numpy as np from scipy.ndimage import binary_dilation, binary_erosion -import skimage.filter as F +import skimage.feature as F class TestCanny(unittest.TestCase): diff --git a/skimage/filter/__init__.py b/skimage/filter/__init__.py index 649eba6b..61b697d7 100644 --- a/skimage/filter/__init__.py +++ b/skimage/filter/__init__.py @@ -1,6 +1,7 @@ from .lpi_filter import inverse, wiener, LPIFilter2D from ._gaussian import gaussian_filter -from ._canny import canny +# Backward compatibility v<0.10 +from ..feature._canny import canny from .edges import (sobel, hsobel, vsobel, scharr, hscharr, vscharr, prewitt, hprewitt, vprewitt, roberts, roberts_positive_diagonal, roberts_negative_diagonal) diff --git a/skimage/viewer/plugins/canny.py b/skimage/viewer/plugins/canny.py index c2294ba8..83e80890 100644 --- a/skimage/viewer/plugins/canny.py +++ b/skimage/viewer/plugins/canny.py @@ -1,7 +1,7 @@ import numpy as np import skimage -from skimage.filter import canny +from skimage.feature import canny from .overlayplugin import OverlayPlugin from ..widgets import Slider, ComboBox From 23a6f0af6c54e3bcab7c58e610e77da8acfdadf3 Mon Sep 17 00:00:00 2001 From: Julien Coste Date: Sun, 31 Aug 2014 11:00:16 +0100 Subject: [PATCH 0281/1122] remove auto added __author__ in __init__.py file --- skimage/feature/tests/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skimage/feature/tests/__init__.py b/skimage/feature/tests/__init__.py index fc970954..e69de29b 100644 --- a/skimage/feature/tests/__init__.py +++ b/skimage/feature/tests/__init__.py @@ -1 +0,0 @@ -__author__ = 'julien' From af59d06461b999bcf9bd881eabd0da5fd5686d98 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sun, 31 Aug 2014 10:57:51 +0100 Subject: [PATCH 0282/1122] ENH: Simplify marching_cubes for maintainability. This does not change execution time, but the entire algorithm is much simpler to read and understand without the branching Cython code paths for anisotropic inputs. Memory usage decreases a minor amount. --- skimage/measure/_marching_cubes.py | 9 ++- skimage/measure/_marching_cubes_cy.pyx | 78 ++++++-------------------- 2 files changed, 22 insertions(+), 65 deletions(-) diff --git a/skimage/measure/_marching_cubes.py b/skimage/measure/_marching_cubes.py index 8f9dfcbc..a334fbbd 100644 --- a/skimage/measure/_marching_cubes.py +++ b/skimage/measure/_marching_cubes.py @@ -105,6 +105,9 @@ def marching_cubes(volume, level, spacing=(1., 1., 1.)): raise ValueError("Input volume must have 3 dimensions.") if level < volume.min() or level > volume.max(): raise ValueError("Contour level must be within volume data range.") + if len(spacing) != 3: + raise ValueError("`spacing` must consist of three floats.") + volume = np.array(volume, dtype=np.float64, order="C") # Extract raw triangles using marching cubes in Cython @@ -113,14 +116,14 @@ def marching_cubes(volume, level, spacing=(1., 1., 1.)): # Note: this algorithm is fast, but returns degenerate "triangles" which # have repeated vertices - and equivalent vertices are redundantly # placed in every triangle they connect with. - raw_faces = _marching_cubes_cy.iterate_and_store_3d(volume, float(level), - spacing) + raw_faces = _marching_cubes_cy.iterate_and_store_3d(volume, float(level)) # Find and collect unique vertices, storing triangle verts as indices. # Returns a true mesh with no degenerate faces. verts, faces = _marching_cubes_cy.unpack_unique_verts(raw_faces) - return np.asarray(verts), np.asarray(faces) + # Adjust for non-isotropic spacing in `verts` at time of return + return np.asarray(verts) * np.r_[spacing], np.asarray(faces) def mesh_surface_area(verts, faces): diff --git a/skimage/measure/_marching_cubes_cy.pyx b/skimage/measure/_marching_cubes_cy.pyx index 085108ab..096d502d 100644 --- a/skimage/measure/_marching_cubes_cy.pyx +++ b/skimage/measure/_marching_cubes_cy.pyx @@ -55,33 +55,21 @@ def unpack_unique_verts(list trilist): return vert_list, face_list -def iterate_and_store_3d(double[:, :, ::1] arr, double level, - tuple spacing=(1., 1., 1.)): +def iterate_and_store_3d(double[:, :, ::1] arr, double level): """Iterate across the given array in a marching-cubes fashion, looking for volumes with edges that cross 'level'. If such a volume is found, appropriate triangulations are added to a growing list of faces to be returned by this function. - If `spacing` is not provided, vertices are returned in the indexing - coordinate system (assuming all 3 spatial dimensions sampled equally). - If `spacing` is provided, vertices will be returned in volume coordinates - relative to the origin, regularly spaced as specified in each dimension. - """ if arr.shape[0] < 2 or arr.shape[1] < 2 or arr.shape[2] < 2: raise ValueError("Input array must be at least 2x2x2.") - if len(spacing) != 3: - raise ValueError("`spacing` must be (double, double, double)") cdef list face_list = [] cdef list norm_list = [] cdef Py_ssize_t n - cdef bint odd_spacing, plus_z + cdef bint plus_z plus_z = False - if [float(i) for i in spacing] == [1.0, 1.0, 1.0]: - odd_spacing = False - else: - odd_spacing = True # The plan is to iterate a 2x2x2 cube across the input array. This means # the upper-left corner of the cube needs to iterate across a sub-array @@ -107,12 +95,6 @@ def iterate_and_store_3d(double[:, :, ::1] arr, double level, coords[1] = 0 coords[2] = 0 - # Extract doubles from `spacing` for speed - cdef double[3] spacing2 - spacing2[0] = spacing[0] - spacing2[1] = spacing[1] - spacing2[2] = spacing[2] - # Calculate the number of iterations we'll need cdef Py_ssize_t num_cube_steps = ((arr.shape[0] - 1) * (arr.shape[1] - 1) * @@ -120,7 +102,7 @@ def iterate_and_store_3d(double[:, :, ::1] arr, double level, cdef unsigned char cube_case = 0 cdef tuple e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12 - cdef double v1, v2, v3, v4, v5, v6, v7, v8, r0, r1, c0, c1, d0, d1 + cdef double v1, v2, v3, v4, v5, v6, v7, v8 cdef Py_ssize_t x0, y0, z0, x1, y1, z1 e5, e6, e7, e8 = (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0) @@ -138,18 +120,6 @@ def iterate_and_store_3d(double[:, :, ::1] arr, double level, x0, y0, z0 = coords[0], coords[1], coords[2] x1, y1, z1 = x0 + 1, y0 + 1, z0 + 1 - if odd_spacing: - # These doubles are the modified world coordinates; they are only - # calculated if non-default `spacing` provided. - r0 = coords[0] * spacing2[0] - c0 = coords[1] * spacing2[1] - d0 = coords[2] * spacing2[2] - r1 = r0 + spacing2[0] - c1 = c0 + spacing2[1] - d1 = d0 + spacing2[2] - else: - r0, c0, d0, r1, c1, d1 = x0, y0, z0, x1, y1, z1 - # We use a right-handed coordinate system, UNlike the paper, but want # to index in agreement - the coordinate adjustment takes place here. v1 = arr[x0, y0, z0] @@ -192,40 +162,24 @@ def iterate_and_store_3d(double[:, :, ::1] arr, double level, e3 = e7 e4 = e8 else: - # Calculate edges normally - if odd_spacing: - e1 = r0 + _get_fraction(v1, v2, level) * spacing2[0], c0, d0 - e2 = r1, c0 + _get_fraction(v2, v3, level) * spacing2[1], d0 - e3 = r0 + _get_fraction(v4, v3, level) * spacing2[0], c1, d0 - e4 = r0, c0 + _get_fraction(v1, v4, level) * spacing2[1], d0 - else: - e1 = r0 + _get_fraction(v1, v2, level), c0, d0 - e2 = r1, c0 + _get_fraction(v2, v3, level), d0 - e3 = r0 + _get_fraction(v4, v3, level), c1, d0 - e4 = r0, c0 + _get_fraction(v1, v4, level), d0 + # Calculate these edges normally + e1 = x0 + _get_fraction(v1, v2, level), y0, z0 + e2 = x1, y0 + _get_fraction(v2, v3, level), z0 + e3 = x0 + _get_fraction(v4, v3, level), y1, z0 + e4 = x0, y0 + _get_fraction(v1, v4, level), z0 # These must be calculated at each point unless we implemented a # large, growing lookup table for all adjacent values; could save # ~30% in terms of runtime at the expense of memory usage and # much greater complexity. - if odd_spacing: - e5 = r0 + _get_fraction(v5, v6, level) * spacing2[0], c0, d1 - e6 = r1, c0 + _get_fraction(v6, v7, level) * spacing2[1], d1 - e7 = r0 + _get_fraction(v8, v7, level) * spacing2[0], c1, d1 - e8 = r0, c0 + _get_fraction(v5, v8, level) * spacing2[1], d1 - e9 = r0, c0, d0 + _get_fraction(v1, v5, level) * spacing2[2] - e10 = r1, c0, d0 + _get_fraction(v2, v6, level) * spacing2[2] - e11 = r0, c1, d0 + _get_fraction(v4, v8, level) * spacing2[2] - e12 = r1, c1, d0 + _get_fraction(v3, v7, level) * spacing2[2] - else: - e5 = r0 + _get_fraction(v5, v6, level), c0, d1 - e6 = r1, c0 + _get_fraction(v6, v7, level), d1 - e7 = r0 + _get_fraction(v8, v7, level), c1, d1 - e8 = r0, c0 + _get_fraction(v5, v8, level), d1 - e9 = r0, c0, d0 + _get_fraction(v1, v5, level) - e10 = r1, c0, d0 + _get_fraction(v2, v6, level) - e11 = r0, c1, d0 + _get_fraction(v4, v8, level) - e12 = r1, c1, d0 + _get_fraction(v3, v7, level) + e5 = x0 + _get_fraction(v5, v6, level), y0, z1 + e6 = x1, y0 + _get_fraction(v6, v7, level), z1 + e7 = x0 + _get_fraction(v8, v7, level), y1, z1 + e8 = x0, y0 + _get_fraction(v5, v8, level), z1 + e9 = x0, y0, z0 + _get_fraction(v1, v5, level) + e10 = x1, y0, z0 + _get_fraction(v2, v6, level) + e11 = x0, y1, z0 + _get_fraction(v4, v8, level) + e12 = x1, y1, z0 + _get_fraction(v3, v7, level) # Append appropriate triangles to the growing output `face_list` From b13ea2288a05973ac2cf7b8ba8784a220c2fceb9 Mon Sep 17 00:00:00 2001 From: Julien Coste Date: Sun, 31 Aug 2014 11:01:08 +0100 Subject: [PATCH 0283/1122] fix wrong version number --- skimage/filter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/filter/__init__.py b/skimage/filter/__init__.py index 61b697d7..a611cb6c 100644 --- a/skimage/filter/__init__.py +++ b/skimage/filter/__init__.py @@ -1,6 +1,6 @@ from .lpi_filter import inverse, wiener, LPIFilter2D from ._gaussian import gaussian_filter -# Backward compatibility v<0.10 +# Backward compatibility v<0.11 from ..feature._canny import canny from .edges import (sobel, hsobel, vsobel, scharr, hscharr, vscharr, prewitt, hprewitt, vprewitt, roberts, roberts_positive_diagonal, From 5ccc0d0000504faa57a6d7d10cd34c5fa7a741e8 Mon Sep 17 00:00:00 2001 From: Julien Coste Date: Sun, 31 Aug 2014 11:09:42 +0100 Subject: [PATCH 0284/1122] update api_changes --- doc/source/api_changes.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/source/api_changes.txt b/doc/source/api_changes.txt index 0271bec0..063ab10c 100644 --- a/doc/source/api_changes.txt +++ b/doc/source/api_changes.txt @@ -1,3 +1,7 @@ +Version 0.11 +------------ +- Move ``skimage.filter.canny`` to ``skimage.feature.canny`` + Version 0.10 ------------ - Removed ``skimage.io.video`` functionality due to broken gstreamer bindings From bc3563c038e5206af3ddff452e523bf46ea4de16 Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Sun, 31 Aug 2014 11:18:09 +0100 Subject: [PATCH 0285/1122] Added gitwash as an option on the makefile. --- doc/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/Makefile b/doc/Makefile index a593aa56..befa2087 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -30,7 +30,7 @@ help: @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" - + @echo " gitwash to update the gitwash documentation" clean: -rm -rf $(DEST)/* -rm -rf source/api From d4297c96ca9c19f5bb951826a24a1629a34553f0 Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Sun, 31 Aug 2014 11:18:36 +0100 Subject: [PATCH 0286/1122] Updated the gitwash build. --- doc/source/gitwash/development_workflow.txt | 6 ++-- doc/source/gitwash/forking_hell.txt | 16 +++++------ doc/source/gitwash/git_install.txt | 2 +- doc/source/gitwash/git_intro.txt | 2 +- doc/source/gitwash/git_links.inc | 32 ++++++++++----------- doc/source/gitwash/index.txt | 2 +- doc/source/gitwash/patching.txt | 6 ++-- doc/source/gitwash/set_up_fork.txt | 2 +- doc/source/gitwash/this_project.inc | 2 +- 9 files changed, 35 insertions(+), 35 deletions(-) diff --git a/doc/source/gitwash/development_workflow.txt b/doc/source/gitwash/development_workflow.txt index 47bdead0..5991b61e 100644 --- a/doc/source/gitwash/development_workflow.txt +++ b/doc/source/gitwash/development_workflow.txt @@ -4,7 +4,7 @@ Development workflow #################### -You already have your own forked copy of the scikit-image_ repository, by +You already have your own forked copy of the `scikit-image`_ repository, by following :ref:`forking`. You have :ref:`set-up-fork`. You have configured git by following :ref:`configure-git`. Now you are ready for some real work. @@ -22,7 +22,7 @@ In what follows we'll refer to the upstream scikit-image ``master`` branch, as * Name your branch for the purpose of the changes - e.g. ``bugfix-for-issue-14`` or ``refactor-database-code``. * If you can possibly avoid it, avoid merging trunk or any other branches into - your feature branch while you are working. + your feature branch while you are working. * If you do find yourself merging from trunk, consider :ref:`rebase-on-trunk` * Ask on the `scikit-image mailing list`_ if you get stuck. * Ask for code review! @@ -81,7 +81,7 @@ what the changes in the branch are for. For example ``add-ability-to-fly``, or git checkout my-new-feature Generally, you will want to keep your feature branches on your public github_ -fork of scikit-image_. To do this, you `git push`_ this new branch up to your +fork of `scikit-image`_. To do this, you `git push`_ this new branch up to your github repo. Generally (if you followed the instructions in these pages, and by default), git will have a link to your github repo, called ``origin``. You push up to your own repo on github with:: diff --git a/doc/source/gitwash/forking_hell.txt b/doc/source/gitwash/forking_hell.txt index 02c14b25..2b91c0b9 100644 --- a/doc/source/gitwash/forking_hell.txt +++ b/doc/source/gitwash/forking_hell.txt @@ -1,33 +1,33 @@ .. _forking: -============================================ +====================================================== Making your own copy (fork) of scikit-image -============================================ +====================================================== You need to do this only once. The instructions here are very similar to the instructions at http://help.github.com/forking/ |emdash| please see that page for more detail. We're repeating some of it here just to give the -specifics for the scikit-image_ project, and to suggest some default names. +specifics for the `scikit-image`_ project, and to suggest some default names. Set up and configure a github account -====================================== +===================================== If you don't have a github account, go to the github page, and make one. You then need to configure your account to allow write access |emdash| see the ``Generating SSH keys`` help on `github help`_. -Create your own forked copy of scikit-image_ -============================================= +Create your own forked copy of `scikit-image`_ +====================================================== #. Log into your github account. -#. Go to the scikit-image_ github home at `scikit-image github`_. +#. Go to the `scikit-image`_ github home at `scikit-image github`_. #. Click on the *fork* button: .. image:: forking_button.png Now, after a short pause and some 'Hardcore forking action', you - should find yourself at the home page for your own forked copy of scikit-image_. + should find yourself at the home page for your own forked copy of `scikit-image`_. .. include:: links.inc diff --git a/doc/source/gitwash/git_install.txt b/doc/source/gitwash/git_install.txt index 72a6b911..d39003ec 100644 --- a/doc/source/gitwash/git_install.txt +++ b/doc/source/gitwash/git_install.txt @@ -8,7 +8,7 @@ Overview ======== ================ ============= -Debian / Ubuntu ``sudo apt-get install git-core`` +Debian / Ubuntu ``sudo apt-get install git`` Fedora ``sudo yum install git-core`` Windows Download and install msysGit_ OS X Use the git-osx-installer_ diff --git a/doc/source/gitwash/git_intro.txt b/doc/source/gitwash/git_intro.txt index 8b4d42bc..f98cb712 100644 --- a/doc/source/gitwash/git_intro.txt +++ b/doc/source/gitwash/git_intro.txt @@ -2,7 +2,7 @@ Introduction ============== -These pages describe a git_ and github_ workflow for the scikit-image_ +These pages describe a git_ and github_ workflow for the `scikit-image`_ project. There are several different workflows here, for different ways of diff --git a/doc/source/gitwash/git_links.inc b/doc/source/gitwash/git_links.inc index 3822fd9d..8e628ae1 100644 --- a/doc/source/gitwash/git_links.inc +++ b/doc/source/gitwash/git_links.inc @@ -20,27 +20,27 @@ .. _git svn crash course: http://git-scm.com/course/svn.html .. _learn.github: http://learn.github.com/ .. _network graph visualizer: http://github.com/blog/39-say-hello-to-the-network-graph-visualizer -.. _git user manual: http://www.kernel.org/pub/software/scm/git/docs/user-manual.html -.. _git tutorial: http://www.kernel.org/pub/software/scm/git/docs/gittutorial.html +.. _git user manual: http://schacon.github.com/git/user-manual.html +.. _git tutorial: http://schacon.github.com/git/gittutorial.html .. _git community book: http://book.git-scm.com/ .. _git ready: http://www.gitready.com/ .. _git casts: http://www.gitcasts.com/ .. _Fernando's git page: http://www.fperez.org/py4science/git.html .. _git magic: http://www-cs-students.stanford.edu/~blynn/gitmagic/index.html .. _git concepts: http://www.eecs.harvard.edu/~cduan/technical/git/ -.. _git clone: http://www.kernel.org/pub/software/scm/git/docs/git-clone.html -.. _git checkout: http://www.kernel.org/pub/software/scm/git/docs/git-checkout.html -.. _git commit: http://www.kernel.org/pub/software/scm/git/docs/git-commit.html -.. _git push: http://www.kernel.org/pub/software/scm/git/docs/git-push.html -.. _git pull: http://www.kernel.org/pub/software/scm/git/docs/git-pull.html -.. _git add: http://www.kernel.org/pub/software/scm/git/docs/git-add.html -.. _git status: http://www.kernel.org/pub/software/scm/git/docs/git-status.html -.. _git diff: http://www.kernel.org/pub/software/scm/git/docs/git-diff.html -.. _git log: http://www.kernel.org/pub/software/scm/git/docs/git-log.html -.. _git branch: http://www.kernel.org/pub/software/scm/git/docs/git-branch.html -.. _git remote: http://www.kernel.org/pub/software/scm/git/docs/git-remote.html -.. _git rebase: http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html -.. _git config: http://www.kernel.org/pub/software/scm/git/docs/git-config.html +.. _git clone: http://schacon.github.com/git/git-clone.html +.. _git checkout: http://schacon.github.com/git/git-checkout.html +.. _git commit: http://schacon.github.com/git/git-commit.html +.. _git push: http://schacon.github.com/git/git-push.html +.. _git pull: http://schacon.github.com/git/git-pull.html +.. _git add: http://schacon.github.com/git/git-add.html +.. _git status: http://schacon.github.com/git/git-status.html +.. _git diff: http://schacon.github.com/git/git-diff.html +.. _git log: http://schacon.github.com/git/git-log.html +.. _git branch: http://schacon.github.com/git/git-branch.html +.. _git remote: http://schacon.github.com/git/git-remote.html +.. _git rebase: http://schacon.github.com/git/git-rebase.html +.. _git config: http://schacon.github.com/git/git-config.html .. _why the -a flag?: http://www.gitready.com/beginner/2009/01/18/the-staging-area.html .. _git staging area: http://www.gitready.com/beginner/2009/01/18/the-staging-area.html .. _tangled working copy problem: http://tomayko.com/writings/the-thing-about-git @@ -50,7 +50,7 @@ .. _git foundation: http://matthew-brett.github.com/pydagogue/foundation.html .. _deleting master on github: http://matthew-brett.github.com/pydagogue/gh_delete_master.html .. _rebase without tears: http://matthew-brett.github.com/pydagogue/rebase_without_tears.html -.. _resolving a merge: http://www.kernel.org/pub/software/scm/git/docs/user-manual.html#resolving-a-merge +.. _resolving a merge: http://schacon.github.com/git/user-manual.html#resolving-a-merge .. _ipython git workflow: http://mail.scipy.org/pipermail/ipython-dev/2010-October/006746.html .. other stuff diff --git a/doc/source/gitwash/index.txt b/doc/source/gitwash/index.txt index 7d26110e..2d285df0 100644 --- a/doc/source/gitwash/index.txt +++ b/doc/source/gitwash/index.txt @@ -1,7 +1,7 @@ .. _using-git: Working with *scikit-image* source code -======================================== +================================================ Contents: diff --git a/doc/source/gitwash/patching.txt b/doc/source/gitwash/patching.txt index b40118af..0a317773 100644 --- a/doc/source/gitwash/patching.txt +++ b/doc/source/gitwash/patching.txt @@ -3,7 +3,7 @@ ================ You've discovered a bug or something else you want to change -in scikit-image_ .. |emdash| excellent! +in `scikit-image`_ .. |emdash| excellent! You've worked out a way to fix it |emdash| even better! @@ -57,7 +57,7 @@ In detail git config --global user.name "Your Name Comes Here" #. If you don't already have one, clone a copy of the - scikit-image_ repository:: + `scikit-image`_ repository:: git clone git://github.com/scikit-image/scikit-image.git cd scikit-image @@ -115,7 +115,7 @@ more feature branches, you will probably want to switch to development mode. You can do this with the repository you have. -Fork the scikit-image_ repository on github |emdash| :ref:`forking`. +Fork the `scikit-image`_ repository on github |emdash| :ref:`forking`. Then:: # checkout and refresh master branch from main repo diff --git a/doc/source/gitwash/set_up_fork.txt b/doc/source/gitwash/set_up_fork.txt index 36153e7d..2679ec31 100644 --- a/doc/source/gitwash/set_up_fork.txt +++ b/doc/source/gitwash/set_up_fork.txt @@ -49,7 +49,7 @@ Linking your repository to the upstream repo git remote add upstream git://github.com/scikit-image/scikit-image.git ``upstream`` here is just the arbitrary name we're using to refer to the -main scikit-image_ repository at `scikit-image github`_. +main `scikit-image`_ repository at `scikit-image github`_. Note that we've used ``git://`` for the URL rather than ``git@``. The ``git://`` URL is read only. This means we that we can't accidentally diff --git a/doc/source/gitwash/this_project.inc b/doc/source/gitwash/this_project.inc index 349b92ba..36b74c83 100644 --- a/doc/source/gitwash/this_project.inc +++ b/doc/source/gitwash/this_project.inc @@ -1,5 +1,5 @@ .. scikit-image -.. _scikit-image: http://scikit-image.org +.. _`scikit-image`: http://scikit-image.org .. _`scikit-image github`: http://github.com/scikit-image/scikit-image .. _`scikit-image mailing list`: http://groups.google.com/group/scikit-image From 00b5ac6804103ac958b78d15885484ce8103fd8c Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Sun, 31 Aug 2014 11:03:37 +0100 Subject: [PATCH 0287/1122] Some minor documentation enhancements. --- doc/ext/LICENSE.txt | 2 +- doc/source/_static/GitHub-Mark-32px.png | Bin 0 -> 931 bytes doc/source/_templates/navbar.html | 7 ++++++- doc/source/gitwash/known_projects.inc | 2 +- doc/source/install.txt | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 doc/source/_static/GitHub-Mark-32px.png diff --git a/doc/ext/LICENSE.txt b/doc/ext/LICENSE.txt index 3ebab893..589b78dc 100644 --- a/doc/ext/LICENSE.txt +++ b/doc/ext/LICENSE.txt @@ -15,7 +15,7 @@ The file - plot_directive.py -was derived from code in Matplotlib (http://matplotlib.sf.net/), which has the +was derived from code in Matplotlib (http://matplotlib.org), which has the following license: Copyright (c) 2002-2008 John D. Hunter; All Rights Reserved. diff --git a/doc/source/_static/GitHub-Mark-32px.png b/doc/source/_static/GitHub-Mark-32px.png new file mode 100644 index 0000000000000000000000000000000000000000..176ab33e44dbb8e017679d9db2a739fdbd6cd4dc GIT binary patch literal 931 zcmV;U16=%xP) zCe@7+MOQP^#cCfC2#G-`LA5R9g%RaqB^jCF2f0-GcG2_FILy4hzP(Ea2HrX6ne+dj z=Q-y*=N-_cq;YZ`*5FPo#*Mf#n9pDaU*TiCpB4G>Zu|~(0lWmyU>yd#rLKiif))+8`~wg^gK}pX{1IhI16A8lEH$$)hs0lb3Iij#?~$k$^S z?;oFId&(VPO?-8VNvJ9WsM6on0@-Hiuu@K*6MSi|=d=YNJ zPvwkHNHM9qR?LJSpJCJeIT$KKQ!#jnh0YJA3m2A`HBCE9(+9CU>=LGsK#(Ebi#hg&fv2u zi=vv+gQHEoyT@HPiK8{9(FpTv41df)7yPqEH$Q4veEl2{7_L)BI9;P3O5U&z7N zP>tzygu@Nr-PgzHn|vLP8rmdmNaJL%577@|LxXuL!rlhsVr=O{^jm_jI@vpr#>o(l z;fjVulXx;K@?)16`9i#o2O5_Aj*<3lxE+s|7=MTtaYw0y-orF`qdn1=5LRRE6A%Vv6Ebhgp9ULF(0{lRE zN~#W}BiN6dg6gTP$X~mJ#Jd{5by)HpR+baGZY;kcl;+JrwI3hg7mVQ5tjK43An+~j z%8L9*wG~&-@(5O7Pf)GI%XmBF$>lxtf<0JK1+OJg8X{KVkq~*WavYChRnu@Y*x?Ru z;mtHo-oazxxfW+T0G`FKI1x&AM^@x#I{rCrwOam9`5XDr|3JV^5!L_z002ovPDHLk FV1j*Iy3YUr literal 0 HcmV?d00001 diff --git a/doc/source/_templates/navbar.html b/doc/source/_templates/navbar.html index 267af8e7..427d19ff 100644 --- a/doc/source/_templates/navbar.html +++ b/doc/source/_templates/navbar.html @@ -2,4 +2,9 @@
  • Download
  • Gallery
  • Documentation
  • -
  • Source
  • +
  • + + Source +
  • diff --git a/doc/source/gitwash/known_projects.inc b/doc/source/gitwash/known_projects.inc index 29723528..c4c3c42f 100644 --- a/doc/source/gitwash/known_projects.inc +++ b/doc/source/gitwash/known_projects.inc @@ -6,7 +6,7 @@ .. _`PROJECTNAME mailing list`: http://projects.scipy.org/mailman/listinfo/nipy-devel .. numpy -.. _numpy: hhttp://numpy.scipy.org +.. _numpy: http://numpy.org .. _`numpy github`: http://github.com/numpy/numpy .. _`numpy mailing list`: http://mail.scipy.org/mailman/listinfo/numpy-discussion diff --git a/doc/source/install.txt b/doc/source/install.txt index 81cb7340..18c0fdda 100644 --- a/doc/source/install.txt +++ b/doc/source/install.txt @@ -2,7 +2,7 @@ Pre-built installation ---------------------- `Windows binaries -`__ +`__ are kindly provided by Christoph Gohlke. The latest stable release is also included as part of From 812b23d41f8d7e71b962044c640b18e70dbd73e7 Mon Sep 17 00:00:00 2001 From: Rebecca Date: Sun, 31 Aug 2014 11:37:35 +0100 Subject: [PATCH 0288/1122] making in-place addition modification suggested in issue #1089 --- skimage/color/colorconv.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 80f115ee..59e68048 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -1127,7 +1127,8 @@ def separate_stains(rgb, conv_matrix): >>> ihc = data.immunohistochemistry() >>> ihc_hdx = separate_stains(ihc, hdx_from_rgb) """ - rgb = dtype.img_as_float(rgb) + 2 + rgb = dtype.img_as_float(rgb, force_copy=True) + rgb += 2 stains = np.dot(np.reshape(-np.log(rgb), (-1, 3)), conv_matrix) return np.reshape(stains, rgb.shape) From 218eb4e89eaef60124389ba3f7690a526e548b7d Mon Sep 17 00:00:00 2001 From: Julien Coste Date: Sun, 31 Aug 2014 11:48:34 +0100 Subject: [PATCH 0289/1122] fix import inside documentation and update TODO --- TODO.txt | 1 + doc/examples/applications/plot_coins_segmentation.py | 2 +- doc/examples/plot_canny.py | 6 +++--- .../plot_circular_elliptical_hough_transform.py | 11 ++++++----- doc/examples/plot_line_hough_transform.py | 2 +- doc/source/user_guide/tutorial_segmentation.txt | 4 ++-- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/TODO.txt b/TODO.txt index e2fc4218..f17b088f 100644 --- a/TODO.txt +++ b/TODO.txt @@ -3,6 +3,7 @@ Remember to list any API changes below in `doc/source/api_changes.txt`. Version 0.13 ------------ * Remove deprecated `None` defaults for `skimage.exposure.rescale_intensity` +* Remove deprecated `skimage.filter.canny` import in __init__.py that is now in `skimage.feature.canny` Version 0.12 ------------ diff --git a/doc/examples/applications/plot_coins_segmentation.py b/doc/examples/applications/plot_coins_segmentation.py index 49ff399f..50eec0bb 100644 --- a/doc/examples/applications/plot_coins_segmentation.py +++ b/doc/examples/applications/plot_coins_segmentation.py @@ -57,7 +57,7 @@ segmentation. To do this, we first get the edges of features using the Canny edge-detector. """ -from skimage.filter import canny +from skimage.feature import canny edges = canny(coins/255.) fig, ax = plt.subplots(figsize=(4, 3)) diff --git a/doc/examples/plot_canny.py b/doc/examples/plot_canny.py index f1caf264..82a930e5 100644 --- a/doc/examples/plot_canny.py +++ b/doc/examples/plot_canny.py @@ -19,7 +19,7 @@ import numpy as np import matplotlib.pyplot as plt from scipy import ndimage -from skimage import filter +from skimage import feature # Generate noisy image of a square @@ -31,8 +31,8 @@ im = ndimage.gaussian_filter(im, 4) im += 0.2 * np.random.random(im.shape) # Compute the Canny filter for two values of sigma -edges1 = filter.canny(im) -edges2 = filter.canny(im, sigma=3) +edges1 = feature.canny(im) +edges2 = feature.canny(im, sigma=3) # display results fig, (ax1, ax2, ax3) = plt.subplots(nrows=1, ncols=3, figsize=(8, 3)) diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py index fbdd4f2c..9b0be0fb 100755 --- a/doc/examples/plot_circular_elliptical_hough_transform.py +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -37,16 +37,16 @@ Its size is extended by two times the larger radius. import numpy as np import matplotlib.pyplot as plt -from skimage import data, filter, color +from skimage import data, color from skimage.transform import hough_circle -from skimage.feature import peak_local_max +from skimage.feature import peak_local_max, canny from skimage.draw import circle_perimeter from skimage.util import img_as_ubyte # Load picture and detect edges image = img_as_ubyte(data.coins()[0:95, 70:370]) -edges = filter.canny(image, sigma=3, low_threshold=10, high_threshold=50) +edges = canny(image, sigma=3, low_threshold=10, high_threshold=50) fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(5, 2)) @@ -106,14 +106,15 @@ References import matplotlib.pyplot as plt -from skimage import data, filter, color +from skimage import data, color +from skimage.feature import canny from skimage.transform import hough_ellipse from skimage.draw import ellipse_perimeter # Load picture, convert to grayscale and detect edges image_rgb = data.coffee()[0:220, 160:420] image_gray = color.rgb2gray(image_rgb) -edges = filter.canny(image_gray, sigma=2.0, +edges = canny(image_gray, sigma=2.0, low_threshold=0.55, high_threshold=0.8) # Perform a Hough Transform diff --git a/doc/examples/plot_line_hough_transform.py b/doc/examples/plot_line_hough_transform.py index bdb05661..4293c409 100644 --- a/doc/examples/plot_line_hough_transform.py +++ b/doc/examples/plot_line_hough_transform.py @@ -58,7 +58,7 @@ References from skimage.transform import (hough_line, hough_line_peaks, probabilistic_hough_line) -from skimage.filter import canny +from skimage.feature import canny from skimage import data import numpy as np diff --git a/doc/source/user_guide/tutorial_segmentation.txt b/doc/source/user_guide/tutorial_segmentation.txt index cff7c651..d183102f 100644 --- a/doc/source/user_guide/tutorial_segmentation.txt +++ b/doc/source/user_guide/tutorial_segmentation.txt @@ -38,11 +38,11 @@ Edge-based segmentation Let us first try to detect edges that enclose the coins. For edge detection, we use the `Canny detector -`_ of ``skimage.filter.canny`` +`_ of ``skimage.feature.canny`` :: - >>> from skimage.filter import canny + >>> from skimage.feature import canny >>> edges = canny(coins/255.) As the background is very smooth, almost all edges are found at the From bfcc27587aad3e0a1ede0a03162bdbd935eda313 Mon Sep 17 00:00:00 2001 From: Julien Coste Date: Sun, 31 Aug 2014 11:59:32 +0100 Subject: [PATCH 0290/1122] fix import in filter.__init__ --- skimage/filter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/filter/__init__.py b/skimage/filter/__init__.py index a611cb6c..9d9fea59 100644 --- a/skimage/filter/__init__.py +++ b/skimage/filter/__init__.py @@ -1,7 +1,7 @@ from .lpi_filter import inverse, wiener, LPIFilter2D from ._gaussian import gaussian_filter # Backward compatibility v<0.11 -from ..feature._canny import canny +from ..feature import canny from .edges import (sobel, hsobel, vsobel, scharr, hscharr, vscharr, prewitt, hprewitt, vprewitt, roberts, roberts_positive_diagonal, roberts_negative_diagonal) From 936211bbcb33af609438b543dfc4bc3a5965feac Mon Sep 17 00:00:00 2001 From: Julien Coste Date: Sun, 31 Aug 2014 12:15:47 +0100 Subject: [PATCH 0291/1122] move import to avoid infinte recursion (filter import peak that import feature that import filter) --- skimage/filter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/filter/__init__.py b/skimage/filter/__init__.py index 9d9fea59..4706f258 100644 --- a/skimage/filter/__init__.py +++ b/skimage/filter/__init__.py @@ -1,7 +1,6 @@ from .lpi_filter import inverse, wiener, LPIFilter2D from ._gaussian import gaussian_filter # Backward compatibility v<0.11 -from ..feature import canny from .edges import (sobel, hsobel, vsobel, scharr, hscharr, vscharr, prewitt, hprewitt, vprewitt, roberts, roberts_positive_diagonal, roberts_negative_diagonal) @@ -10,6 +9,7 @@ from ._gabor import gabor_kernel, gabor_filter from .thresholding import (threshold_adaptive, threshold_otsu, threshold_yen, threshold_isodata) from . import rank +from ..feature import canny from skimage._shared.utils import deprecated From f86b6212bc733afe189b26d6fc87be07f1a06c44 Mon Sep 17 00:00:00 2001 From: Julien Coste Date: Sun, 31 Aug 2014 12:27:02 +0100 Subject: [PATCH 0292/1122] Canny is now deprecated and will be in api_changes in v0.13 --- TODO.txt | 2 +- doc/source/api_changes.txt | 4 ---- skimage/filter/__init__.py | 7 ++++--- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/TODO.txt b/TODO.txt index f17b088f..e8e70e6c 100644 --- a/TODO.txt +++ b/TODO.txt @@ -3,7 +3,7 @@ Remember to list any API changes below in `doc/source/api_changes.txt`. Version 0.13 ------------ * Remove deprecated `None` defaults for `skimage.exposure.rescale_intensity` -* Remove deprecated `skimage.filter.canny` import in __init__.py that is now in `skimage.feature.canny` +* Remove deprecated `skimage.filter.canny` import in __init__.py that is now in `skimage.feature.canny` (and complete api_changes.txt. `GitHub discuss `__ ) Version 0.12 ------------ diff --git a/doc/source/api_changes.txt b/doc/source/api_changes.txt index 063ab10c..0271bec0 100644 --- a/doc/source/api_changes.txt +++ b/doc/source/api_changes.txt @@ -1,7 +1,3 @@ -Version 0.11 ------------- -- Move ``skimage.filter.canny`` to ``skimage.feature.canny`` - Version 0.10 ------------ - Removed ``skimage.io.video`` functionality due to broken gstreamer bindings diff --git a/skimage/filter/__init__.py b/skimage/filter/__init__.py index 4706f258..75bbb6b9 100644 --- a/skimage/filter/__init__.py +++ b/skimage/filter/__init__.py @@ -1,6 +1,5 @@ from .lpi_filter import inverse, wiener, LPIFilter2D from ._gaussian import gaussian_filter -# Backward compatibility v<0.11 from .edges import (sobel, hsobel, vsobel, scharr, hscharr, vscharr, prewitt, hprewitt, vprewitt, roberts, roberts_positive_diagonal, roberts_negative_diagonal) @@ -9,8 +8,6 @@ from ._gabor import gabor_kernel, gabor_filter from .thresholding import (threshold_adaptive, threshold_otsu, threshold_yen, threshold_isodata) from . import rank -from ..feature import canny - from skimage._shared.utils import deprecated from skimage import restoration @@ -21,6 +18,10 @@ denoise_tv_bregman = deprecated('skimage.restoration.denoise_tv_bregman')\ denoise_tv_chambolle = deprecated('skimage.restoration.denoise_tv_chambolle')\ (restoration.denoise_tv_chambolle) +# Backward compatibility v<0.11 +from ..feature import canny +canny = deprecated('skimage.feature.canny')(canny) + __all__ = ['inverse', 'wiener', From f1a6d2dbf1804224285f5c1965a5124ecfc2abe0 Mon Sep 17 00:00:00 2001 From: Stuart Mumford Date: Sun, 31 Aug 2014 14:53:43 +0100 Subject: [PATCH 0293/1122] Website font readability improvements. Increase font size by 1% Increase character spacing Increase font size in code snippets to website standard --- .../themes/scikit-image/static/css/custom.css | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/doc/source/themes/scikit-image/static/css/custom.css b/doc/source/themes/scikit-image/static/css/custom.css index 0a2d3136..12eb642b 100644 --- a/doc/source/themes/scikit-image/static/css/custom.css +++ b/doc/source/themes/scikit-image/static/css/custom.css @@ -1,5 +1,7 @@ body { font-family: "Raleway"; + letter-spacing: 0.02em; + font-size: 102%; } a { color: #CE5C00; @@ -10,9 +12,6 @@ select, textarea { font-family: "Raleway"; } -pre { - font-size: 11px; -} h1, h2, h3, h4, h5, h6 { clear: left; @@ -41,12 +40,6 @@ h6 { font-size: 13px; line-height: 15px; } -blockquote { - border-left: 0; -} -dt { - font-weight: normal; -} .logo { float: left; @@ -57,18 +50,21 @@ dt { } .hero { - padding: 10px 25px; + padding: 10px 25px 15px 25px; } .gallery-random { float: right; line-height: 180px; + margin-left: 10px; } .gallery-random img { max-height: 180px; + max-width: 180px; } .coins-sample { + float: left; padding: 5px; } @@ -79,10 +75,6 @@ dt { padding-left: 15px; } -#current { - font-weight: bold; -} - .headerlink { margin-left: 10px; color: #ddd; @@ -111,7 +103,7 @@ h6:hover .headerlink { text-decoration: underline; } -.ohloh-use, .gplus-use { +.gplus-use { float: left; margin: 0 0 10px 15px; } @@ -233,7 +225,14 @@ p.admonition-title { text-align: center !important; } -/* misc */ -div.math { - text-align: center; +.summary-box { + /* Should derive width from span8 */ + width: 640px; +} + +.citation { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +/* padding: 1em;*/ } From f87db0a1ecf53faf038b421ad9c2a69b1e240a08 Mon Sep 17 00:00:00 2001 From: Geoffrey French Date: Sun, 31 Aug 2014 15:12:01 +0100 Subject: [PATCH 0294/1122] Fixes a bug in _update_doc in skimage/io/__init__.py that will attempt to compute max of empty list if no plugins are found. --- skimage/io/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/io/__init__.py b/skimage/io/__init__.py index 144fef57..8f3b9bca 100644 --- a/skimage/io/__init__.py +++ b/skimage/io/__init__.py @@ -40,7 +40,8 @@ def _update_doc(doc): info_table = [(p, plugin_info(p).get('description', 'no description')) for p in available_plugins if not p == 'test'] - name_length = max([len(n) for (n, _) in info_table]) + name_length = max([len(n) for (n, _) in info_table]) if len(info_table) > 0 else 0 + description_length = WRAP_LEN - 1 - name_length column_lengths = [name_length, description_length] _format_plugin_info_table(info_table, column_lengths) From f8a0f272657f95c06911e8d50155f0df4e902317 Mon Sep 17 00:00:00 2001 From: Julien Coste Date: Sun, 31 Aug 2014 15:21:46 +0100 Subject: [PATCH 0295/1122] hack to avoid circular import when import canny --- TODO.txt | 3 ++- skimage/filter/__init__.py | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/TODO.txt b/TODO.txt index e8e70e6c..3e6b3a7e 100644 --- a/TODO.txt +++ b/TODO.txt @@ -3,7 +3,8 @@ Remember to list any API changes below in `doc/source/api_changes.txt`. Version 0.13 ------------ * Remove deprecated `None` defaults for `skimage.exposure.rescale_intensity` -* Remove deprecated `skimage.filter.canny` import in __init__.py that is now in `skimage.feature.canny` (and complete api_changes.txt. `GitHub discuss `__ ) +* Remove deprecated `skimage.filter.canny` import in filter/__init__.py file (canny is now in `skimage.feature.canny`). + * Don't forget to complete api_changes.txt. (`GitHub discuss `__ ) Version 0.12 ------------ diff --git a/skimage/filter/__init__.py b/skimage/filter/__init__.py index 75bbb6b9..0228957e 100644 --- a/skimage/filter/__init__.py +++ b/skimage/filter/__init__.py @@ -19,8 +19,11 @@ denoise_tv_chambolle = deprecated('skimage.restoration.denoise_tv_chambolle')\ (restoration.denoise_tv_chambolle) # Backward compatibility v<0.11 -from ..feature import canny -canny = deprecated('skimage.feature.canny')(canny) +@deprecated +def canny(*args, **kwargs): + # Hack to avoid circular import + from skimage.feature._canny import canny as canny_ + return canny_(*args, **kwargs) __all__ = ['inverse', From a85a8bcd3967c20e7c53b7c6bb43e824ed021d04 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 31 Aug 2014 09:50:25 -0500 Subject: [PATCH 0296/1122] Fix #1108 --- skimage/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/__init__.py b/skimage/__init__.py index 59c80ed2..d1f98ff7 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -69,7 +69,8 @@ try: from .version import version as __version__ except ImportError: __version__ = "unbuilt-dev" -del version +else: + del version try: From d63d89497bd9a6e3b924c3db155be4430347a584 Mon Sep 17 00:00:00 2001 From: Geoffrey French Date: Sun, 31 Aug 2014 16:04:38 +0100 Subject: [PATCH 0297/1122] Modified rank filters package so that the _core function in core.pyx outputs to a 3D image, permitting the generation of images with arbitrary size feature vector pixels. Implemented windowed_histogram that generates a windowed histogram of an image. --- skimage/filter/rank/__init__.py | 5 +- skimage/filter/rank/_percentile.py | 2 +- skimage/filter/rank/bilateral.py | 2 +- skimage/filter/rank/bilateral_cy.pyx | 55 ++-- skimage/filter/rank/core_cy.pxd | 8 +- skimage/filter/rank/core_cy.pyx | 28 +- skimage/filter/rank/generic.py | 99 +++++-- skimage/filter/rank/generic_cy.pyx | 377 ++++++++++++++------------ skimage/filter/rank/percentile_cy.pyx | 176 ++++++------ 9 files changed, 431 insertions(+), 321 deletions(-) diff --git a/skimage/filter/rank/__init__.py b/skimage/filter/rank/__init__.py index 8641b984..22b0e881 100644 --- a/skimage/filter/rank/__init__.py +++ b/skimage/filter/rank/__init__.py @@ -1,6 +1,6 @@ from .generic import (autolevel, bottomhat, equalize, gradient, maximum, mean, subtract_mean, median, minimum, modal, enhance_contrast, - pop, threshold, tophat, noise_filter, entropy, otsu, sum) + pop, threshold, tophat, noise_filter, entropy, otsu, sum, windowed_histogram) from ._percentile import (autolevel_percentile, gradient_percentile, mean_percentile, subtract_mean_percentile, enhance_contrast_percentile, percentile, @@ -37,4 +37,5 @@ __all__ = ['autolevel', 'noise_filter', 'entropy', 'otsu', - 'percentile'] + 'percentile', + 'windowed_histogram'] diff --git a/skimage/filter/rank/_percentile.py b/skimage/filter/rank/_percentile.py index 01dd0b49..9b93195b 100644 --- a/skimage/filter/rank/_percentile.py +++ b/skimage/filter/rank/_percentile.py @@ -43,7 +43,7 @@ def _apply(func, image, selem, out, mask, shift_x, shift_y, p0, p1, func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, out=out, max_bin=max_bin, p0=p0, p1=p1) - return out + return out.reshape(out.shape[:2]) def autolevel_percentile(image, selem, out=None, mask=None, shift_x=False, diff --git a/skimage/filter/rank/bilateral.py b/skimage/filter/rank/bilateral.py index d01680db..73147c7f 100644 --- a/skimage/filter/rank/bilateral.py +++ b/skimage/filter/rank/bilateral.py @@ -42,7 +42,7 @@ def _apply(func, image, selem, out, mask, shift_x, shift_y, s0, s1, func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, out=out, max_bin=max_bin, s0=s0, s1=s1) - return out + return out.reshape(out.shape[:2]) def mean_bilateral(image, selem, out=None, mask=None, shift_x=False, diff --git a/skimage/filter/rank/bilateral_cy.pyx b/skimage/filter/rank/bilateral_cy.pyx index 25a81766..0e232465 100644 --- a/skimage/filter/rank/bilateral_cy.pyx +++ b/skimage/filter/rank/bilateral_cy.pyx @@ -9,10 +9,11 @@ from libc.math cimport log from .core_cy cimport dtype_t, dtype_t_out, _core -cdef inline double _kernel_mean(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_mean(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t bilat_pop = 0 @@ -24,17 +25,18 @@ cdef inline double _kernel_mean(Py_ssize_t* histo, double pop, dtype_t g, bilat_pop += histo[i] mean += histo[i] * i if bilat_pop: - return mean / bilat_pop + out[0] = mean / bilat_pop else: - return 0 + out[0] = 0 else: - return 0 + out[0] = 0 -cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_pop(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t bilat_pop = 0 @@ -43,14 +45,15 @@ cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, for i in range(max_bin): if (g > (i - s0)) and (g < (i + s1)): bilat_pop += histo[i] - return bilat_pop + out[0] = bilat_pop else: - return 0 + out[0] = 0 -cdef inline double _kernel_sum(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_sum(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t bilat_pop = 0 @@ -62,40 +65,40 @@ cdef inline double _kernel_sum(Py_ssize_t* histo, double pop, dtype_t g, bilat_pop += histo[i] sum += histo[i] * i if bilat_pop: - return sum + out[0] = sum else: - return 0 + out[0] = 0 else: - return 0 + out[0] = 0 def _mean(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t s0, Py_ssize_t s1, Py_ssize_t max_bin): - _core(_kernel_mean[dtype_t], image, selem, mask, out, + _core(_kernel_mean[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, s0, s1, max_bin) def _pop(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t s0, Py_ssize_t s1, Py_ssize_t max_bin): - _core(_kernel_pop[dtype_t], image, selem, mask, out, + _core(_kernel_pop[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, s0, s1, max_bin) def _sum(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t s0, Py_ssize_t s1, Py_ssize_t max_bin): - _core(_kernel_sum[dtype_t], image, selem, mask, out, + _core(_kernel_sum[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, s0, s1, max_bin) diff --git a/skimage/filter/rank/core_cy.pxd b/skimage/filter/rank/core_cy.pxd index 2e97e50a..7d23f12a 100644 --- a/skimage/filter/rank/core_cy.pxd +++ b/skimage/filter/rank/core_cy.pxd @@ -15,13 +15,13 @@ cdef dtype_t _max(dtype_t a, dtype_t b) cdef dtype_t _min(dtype_t a, dtype_t b) -cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, - Py_ssize_t, Py_ssize_t, double, - double, Py_ssize_t, Py_ssize_t), +cdef void _core(void kernel(dtype_t_out[:] out, Py_ssize_t*, double, + dtype_t, Py_ssize_t, Py_ssize_t, double, + double, Py_ssize_t, Py_ssize_t), dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, double p0, double p1, Py_ssize_t s0, Py_ssize_t s1, diff --git a/skimage/filter/rank/core_cy.pyx b/skimage/filter/rank/core_cy.pyx index 02c2c8d0..3c8c9e42 100644 --- a/skimage/filter/rank/core_cy.pyx +++ b/skimage/filter/rank/core_cy.pyx @@ -42,13 +42,13 @@ cdef inline char is_in_mask(Py_ssize_t rows, Py_ssize_t cols, return 0 -cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, - Py_ssize_t, Py_ssize_t, double, - double, Py_ssize_t, Py_ssize_t), +cdef void _core(void kernel(dtype_t_out[:] out_feat, Py_ssize_t*, double, dtype_t, + Py_ssize_t, Py_ssize_t, double, + double, Py_ssize_t, Py_ssize_t), dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, double p0, double p1, Py_ssize_t s0, Py_ssize_t s1, @@ -151,8 +151,8 @@ cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, r = 0 c = 0 - out[r, c] = kernel(histo, pop, image[r, c], max_bin, mid_bin, - p0, p1, s0, s1) + kernel(out[r, c, :], histo, pop, image[r, c], max_bin, mid_bin, + p0, p1, s0, s1) # main loop r = 0 @@ -172,8 +172,8 @@ cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - out[r, c] = kernel(histo, pop, image[r, c], - max_bin, mid_bin, p0, p1, s0, s1) + kernel(out[r, c, :], histo, pop, image[r, c], max_bin, mid_bin, + p0, p1, s0, s1) r += 1 # pass to the next row if r >= rows: @@ -192,8 +192,8 @@ cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - out[r, c] = kernel(histo, pop, image[r, c], - max_bin, mid_bin, p0, p1, s0, s1) + kernel(out[r, c, :], histo, pop, image[r, c], max_bin, mid_bin, + p0, p1, s0, s1) # ---> east to west for c in range(cols - 2, -1, -1): @@ -209,8 +209,8 @@ cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - out[r, c] = kernel(histo, pop, image[r, c], - max_bin, mid_bin, p0, p1, s0, s1) + kernel(out[r, c, :], histo, pop, image[r, c], max_bin, mid_bin, + p0, p1, s0, s1) r += 1 # pass to the next row if r >= rows: @@ -229,8 +229,8 @@ cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - out[r, c] = kernel(histo, pop, image[r, c], - max_bin, mid_bin, p0, p1, s0, s1) + kernel(out[r, c, :], histo, pop, image[r, c], + max_bin, mid_bin, p0, p1, s0, s1) # release memory allocated by malloc free(se_e_r) diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index ccc1166e..07c592af 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -28,7 +28,7 @@ __all__ = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', 'mean', 'pop', 'threshold', 'tophat', 'noise_filter', 'entropy', 'otsu'] -def _handle_input(image, selem, out, mask, out_dtype=None): +def _handle_input(image, selem, out, mask, out_dtype=None, pixel_size=1): if image.dtype not in (np.uint8, np.uint16): image = img_as_ubyte(image) @@ -45,7 +45,10 @@ def _handle_input(image, selem, out, mask, out_dtype=None): if out is None: if out_dtype is None: out_dtype = image.dtype - out = np.empty_like(image, dtype=out_dtype) + out = np.empty(image.shape+(pixel_size,), dtype=out_dtype) + else: + if len(out.shape) == 2: + out = out.reshape(out.shape+(pixel_size,)) if image is out: raise NotImplementedError("Cannot perform rank operation in place.") @@ -65,7 +68,7 @@ def _handle_input(image, selem, out, mask, out_dtype=None): return image, selem, out, mask, max_bin -def _apply(func, image, selem, out, mask, shift_x, shift_y, out_dtype=None): +def _apply_scalar_per_pixel(func, image, selem, out, mask, shift_x, shift_y, out_dtype=None): image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, out_dtype) @@ -73,6 +76,17 @@ def _apply(func, image, selem, out, mask, shift_x, shift_y, out_dtype=None): func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, out=out, max_bin=max_bin) + return out.reshape(out.shape[:2]) + + +def _apply_vector_per_pixel(func, image, selem, out, mask, shift_x, shift_y, out_dtype=None, pixel_size=1): + + image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, + out_dtype, pixel_size=pixel_size) + + func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, + out=out, max_bin=max_bin) + return out @@ -113,7 +127,7 @@ def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._autolevel, image, selem, + return _apply_scalar_per_pixel(generic_cy._autolevel, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -154,7 +168,7 @@ def bottomhat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._bottomhat, image, selem, + return _apply_scalar_per_pixel(generic_cy._bottomhat, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -192,7 +206,7 @@ def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._equalize, image, selem, + return _apply_scalar_per_pixel(generic_cy._equalize, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -230,7 +244,7 @@ def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._gradient, image, selem, + return _apply_scalar_per_pixel(generic_cy._gradient, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -277,7 +291,7 @@ def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._maximum, image, selem, + return _apply_scalar_per_pixel(generic_cy._maximum, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -315,7 +329,7 @@ def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._mean, image, selem, out=out, + return _apply_scalar_per_pixel(generic_cy._mean, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -354,7 +368,7 @@ def subtract_mean(image, selem, out=None, mask=None, shift_x=False, """ - return _apply(generic_cy._subtract_mean, image, selem, + return _apply_scalar_per_pixel(generic_cy._subtract_mean, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -392,7 +406,7 @@ def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._median, image, selem, + return _apply_scalar_per_pixel(generic_cy._median, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -439,7 +453,7 @@ def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._minimum, image, selem, + return _apply_scalar_per_pixel(generic_cy._minimum, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -479,7 +493,7 @@ def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._modal, image, selem, + return _apply_scalar_per_pixel(generic_cy._modal, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -522,7 +536,7 @@ def enhance_contrast(image, selem, out=None, mask=None, shift_x=False, """ - return _apply(generic_cy._enhance_contrast, image, selem, + return _apply_scalar_per_pixel(generic_cy._enhance_contrast, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -571,7 +585,7 @@ def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._pop, image, selem, out=out, + return _apply_scalar_per_pixel(generic_cy._pop, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -620,7 +634,7 @@ def sum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._sum, image, selem, out=out, + return _apply_scalar_per_pixel(generic_cy._sum, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -669,7 +683,7 @@ def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._threshold, image, selem, + return _apply_scalar_per_pixel(generic_cy._threshold, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -710,7 +724,7 @@ def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._tophat, image, selem, + return _apply_scalar_per_pixel(generic_cy._tophat, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -761,7 +775,7 @@ def noise_filter(image, selem, out=None, mask=None, shift_x=False, selem_cpy = selem.copy() selem_cpy[centre_r, centre_c] = 0 - return _apply(generic_cy._noise_filter, image, selem_cpy, out=out, + return _apply_scalar_per_pixel(generic_cy._noise_filter, image, selem_cpy, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -806,7 +820,7 @@ def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._entropy, image, selem, + return _apply_scalar_per_pixel(generic_cy._entropy, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, out_dtype=np.double) @@ -850,5 +864,48 @@ def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic_cy._otsu, image, selem, out=out, + return _apply_scalar_per_pixel(generic_cy._otsu, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def windowed_histogram(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Sliding window histogram + + Parameters + ---------- + image : ndarray + Image array (uint8 array). + selem : 2-D array + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : 3-D array (same dtype as input image) whose extra dimension + Output image. + + References + ---------- + .. [otsu] http://en.wikipedia.org/wiki/Otsu's_method + + Examples + -------- + >>> from skimage import data + >>> from skimage.filter.rank import windowed_histogram + >>> from skimage.morphology import disk + >>> img = data.camera() + >>> hist_img = windowed_histogram(img, disk(5)) + >>> thresh_image = img >= local_otsu + + """ + + return _apply_vector_per_pixel(generic_cy._windowed_hist, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y, pixel_size=image.max()+1) diff --git a/skimage/filter/rank/generic_cy.pyx b/skimage/filter/rank/generic_cy.pyx index 1c26cf53..88766f44 100644 --- a/skimage/filter/rank/generic_cy.pyx +++ b/skimage/filter/rank/generic_cy.pyx @@ -9,10 +9,11 @@ from libc.math cimport log from .core_cy cimport dtype_t, dtype_t_out, _core -cdef inline double _kernel_autolevel(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_autolevel(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, imin, imax, delta @@ -27,17 +28,18 @@ cdef inline double _kernel_autolevel(Py_ssize_t* histo, double pop, dtype_t g, break delta = imax - imin if delta > 0: - return (max_bin - 1) * (g - imin) / delta + out[0] = (max_bin - 1) * (g - imin) / delta else: - return 0 + out[0] = 0 else: - return 0 + out[0] = 0 -cdef inline double _kernel_bottomhat(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_bottomhat(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i @@ -45,15 +47,16 @@ cdef inline double _kernel_bottomhat(Py_ssize_t* histo, double pop, dtype_t g, for i in range(max_bin): if histo[i]: break - return g - i + out[0] = g - i else: - return 0 + out[0] = 0 -cdef inline double _kernel_equalize(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_equalize(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t sum = 0 @@ -63,15 +66,16 @@ cdef inline double _kernel_equalize(Py_ssize_t* histo, double pop, dtype_t g, sum += histo[i] if i >= g: break - return ((max_bin - 1) * sum) / pop + out[0] = (((max_bin - 1) * sum) / pop) else: - return 0 + out[0] = 0 -cdef inline double _kernel_gradient(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_gradient(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, imin, imax @@ -84,65 +88,68 @@ cdef inline double _kernel_gradient(Py_ssize_t* histo, double pop, dtype_t g, if histo[i]: imin = i break - return imax - imin + out[0] = (imax - imin) else: - return 0 + out[0] = 0 -cdef inline double _kernel_maximum(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_maximum(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i if pop: for i in range(max_bin - 1, -1, -1): if histo[i]: - return i + out[0] = i + return else: - return 0 + out[0] = 0 -cdef inline double _kernel_mean(Py_ssize_t* histo, double pop,dtype_t g, +cdef inline void _kernel_mean(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + mean += histo[i] * i + out[0] = (mean / pop) + else: + out[0] = 0 + + +cdef inline void _kernel_subtract_mean(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + mean += histo[i] * i + out[0] = ((g - mean / pop) / 2. + 127) + else: + out[0] = 0 + + +cdef inline void _kernel_median(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - cdef Py_ssize_t mean = 0 - - if pop: - for i in range(max_bin): - mean += histo[i] * i - return mean / pop - else: - return 0 - - -cdef inline double _kernel_subtract_mean(Py_ssize_t* histo, double pop, - dtype_t g, - Py_ssize_t max_bin, - Py_ssize_t mid_bin, double p0, - double p1, Py_ssize_t s0, - Py_ssize_t s1): - - cdef Py_ssize_t i - cdef Py_ssize_t mean = 0 - - if pop: - for i in range(max_bin): - mean += histo[i] * i - return (g - mean / pop) / 2. + 127 - else: - return 0 - - -cdef inline double _kernel_median(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i cdef double sum = pop / 2.0 @@ -151,30 +158,34 @@ cdef inline double _kernel_median(Py_ssize_t* histo, double pop, dtype_t g, if histo[i]: sum -= histo[i] if sum < 0: - return i + out[0] = i + return else: - return 0 + out[0] = 0 -cdef inline double _kernel_minimum(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_minimum(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i if pop: for i in range(max_bin): if histo[i]: - return i + out[0] = i + return else: - return 0 + out[0] = 0 -cdef inline double _kernel_modal(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_modal(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t hmax = 0, imax = 0 @@ -183,17 +194,19 @@ cdef inline double _kernel_modal(Py_ssize_t* histo, double pop, dtype_t g, if histo[i] > hmax: hmax = histo[i] imax = i - return imax + out[0] = imax else: - return 0 + out[0] = 0 -cdef inline double _kernel_enhance_contrast(Py_ssize_t* histo, double pop, - dtype_t g, - Py_ssize_t max_bin, - Py_ssize_t mid_bin, double p0, - double p1, Py_ssize_t s0, - Py_ssize_t s1): +cdef inline void _kernel_enhance_contrast(dtype_t_out[:] out, + Py_ssize_t* histo, + double pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): cdef Py_ssize_t i, imin, imax @@ -207,25 +220,27 @@ cdef inline double _kernel_enhance_contrast(Py_ssize_t* histo, double pop, imin = i break if imax - g < g - imin: - return imax + out[0] = imax else: - return imin + out[0] = imin else: - return 0 + out[0] = 0 -cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_pop(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): - return pop + out[0] = pop -cdef inline double _kernel_sum(Py_ssize_t* histo, double pop,dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_sum(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t sum = 0 @@ -233,15 +248,16 @@ cdef inline double _kernel_sum(Py_ssize_t* histo, double pop,dtype_t g, if pop: for i in range(max_bin): sum += histo[i] * i - return sum + out[0] = sum else: - return 0 + out[0] = 0 -cdef inline double _kernel_threshold(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_threshold(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t mean = 0 @@ -249,15 +265,16 @@ cdef inline double _kernel_threshold(Py_ssize_t* histo, double pop, dtype_t g, if pop: for i in range(max_bin): mean += histo[i] * i - return g > (mean / pop) + out[0] = (g > (mean / pop)) else: - return 0 + out[0] = 0 -cdef inline double _kernel_tophat(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_tophat(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i @@ -265,23 +282,23 @@ cdef inline double _kernel_tophat(Py_ssize_t* histo, double pop, dtype_t g, for i in range(max_bin - 1, -1, -1): if histo[i]: break - return i - g + out[0] = (i - g) else: - return 0 + out[0] = 0 -cdef inline double _kernel_noise_filter(Py_ssize_t* histo, double pop, - dtype_t g, Py_ssize_t max_bin, - Py_ssize_t mid_bin, double p0, - double p1, Py_ssize_t s0, - Py_ssize_t s1): +cdef inline void _kernel_noise_filter(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t min_i # early stop if at least one pixel of the neighborhood has the same g if histo[g] > 0: - return 0 + out[0] = 0 for i in range(g, -1, -1): if histo[i]: @@ -291,15 +308,16 @@ cdef inline double _kernel_noise_filter(Py_ssize_t* histo, double pop, if histo[i]: break if i - g < min_i: - return i - g + out[0] = (i - g) else: - return min_i + out[0] = min_i -cdef inline double _kernel_entropy(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_entropy(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef double e, p @@ -309,15 +327,16 @@ cdef inline double _kernel_entropy(Py_ssize_t* histo, double pop, dtype_t g, p = histo[i] / pop if p > 0: e -= p * log(p) / 0.6931471805599453 - return e + out[0] = e else: - return 0 + out[0] = 0 -cdef inline double _kernel_otsu(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_otsu(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t max_i cdef double P, mu1, mu2, q1, new_q1, sigma_b, max_sigma_b @@ -329,7 +348,7 @@ cdef inline double _kernel_otsu(Py_ssize_t* histo, double pop, dtype_t g, mu += histo[i] * i mu = mu / pop else: - return 0 + out[0] = 0 # maximizing the between class variance max_i = 0 @@ -349,183 +368,205 @@ cdef inline double _kernel_otsu(Py_ssize_t* histo, double pop, dtype_t g, max_i = i q1 = new_q1 - return max_i + out[0] = max_i + + +cdef inline void _kernel_win_hist(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef Py_ssize_t max_i + for i in xrange(out.shape[0]): + out[i] = histo[i] + def _autolevel(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - _core(_kernel_autolevel[dtype_t], image, selem, mask, out, + _core(_kernel_autolevel[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _bottomhat(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - _core(_kernel_bottomhat[dtype_t], image, selem, mask, out, + _core(_kernel_bottomhat[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _equalize(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - _core(_kernel_equalize[dtype_t], image, selem, mask, out, + _core(_kernel_equalize[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _gradient(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - _core(_kernel_gradient[dtype_t], image, selem, mask, out, + _core(_kernel_gradient[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _maximum(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - _core(_kernel_maximum[dtype_t], image, selem, mask, out, + _core(_kernel_maximum[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _mean(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - _core(_kernel_mean[dtype_t], image, selem, mask, out, + _core(_kernel_mean[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _subtract_mean(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - _core(_kernel_subtract_mean[dtype_t], image, selem, mask, + _core(_kernel_subtract_mean[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _median(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - _core(_kernel_median[dtype_t], image, selem, mask, out, + _core(_kernel_median[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _minimum(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - _core(_kernel_minimum[dtype_t], image, selem, mask, out, + _core(_kernel_minimum[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _enhance_contrast(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - _core(_kernel_enhance_contrast[dtype_t], image, selem, mask, + _core(_kernel_enhance_contrast[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _modal(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - _core(_kernel_modal[dtype_t], image, selem, mask, out, + _core(_kernel_modal[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _pop(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - _core(_kernel_pop[dtype_t], image, selem, mask, out, + _core(_kernel_pop[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _sum(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - _core(_kernel_sum[dtype_t], image, selem, mask, + _core(_kernel_sum[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _threshold(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - _core(_kernel_threshold[dtype_t], image, selem, mask, out, + _core(_kernel_threshold[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _tophat(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - _core(_kernel_tophat[dtype_t], image, selem, mask, out, + _core(_kernel_tophat[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _noise_filter(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - _core(_kernel_noise_filter[dtype_t], image, selem, mask, out, + _core(_kernel_noise_filter[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _entropy(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - _core(_kernel_entropy[dtype_t], image, selem, mask, out, + _core(_kernel_entropy[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _otsu(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - _core(_kernel_otsu[dtype_t], image, selem, mask, out, + _core(_kernel_otsu[dtype_t_out, dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _windowed_hist(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, :, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + _core(_kernel_win_hist[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) diff --git a/skimage/filter/rank/percentile_cy.pyx b/skimage/filter/rank/percentile_cy.pyx index 38d04b33..af33546d 100644 --- a/skimage/filter/rank/percentile_cy.pyx +++ b/skimage/filter/rank/percentile_cy.pyx @@ -7,10 +7,11 @@ cimport numpy as cnp from .core_cy cimport dtype_t, dtype_t_out, _core, _min, _max -cdef inline double _kernel_autolevel(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_autolevel(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, imin, imax, sum, delta @@ -31,18 +32,19 @@ cdef inline double _kernel_autolevel(Py_ssize_t* histo, double pop, dtype_t g, delta = imax - imin if delta > 0: - return (max_bin - 1) * (_min(_max(imin, g), imax) - - imin) / delta + out[0] = ((max_bin - 1) * (_min(_max(imin, g), imax) + - imin) / delta) else: - return imax - imin + out[0] = (imax - imin) else: - return 0 + out[0] = 0 -cdef inline double _kernel_gradient(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_gradient(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, imin, imax, sum, delta @@ -61,15 +63,16 @@ cdef inline double _kernel_gradient(Py_ssize_t* histo, double pop, dtype_t g, imax = i break - return imax - imin + out[0] = (imax - imin) else: - return 0 + out[0] = 0 -cdef inline double _kernel_mean(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_mean(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, sum, mean, n @@ -84,16 +87,17 @@ cdef inline double _kernel_mean(Py_ssize_t* histo, double pop, dtype_t g, mean += histo[i] * i if n > 0: - return mean / n + out[0] = (mean / n) else: - return 0 + out[0] = 0 else: - return 0 + out[0] = 0 -cdef inline double _kernel_sum(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_sum(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, sum, sum_g, n @@ -108,18 +112,18 @@ cdef inline double _kernel_sum(Py_ssize_t* histo, double pop, dtype_t g, sum_g += histo[i] * i if n > 0: - return sum_g + out[0] = (sum_g) else: - return 0 + out[0] = 0 else: - return 0 + out[0] = 0 -cdef inline double _kernel_subtract_mean(Py_ssize_t* histo, double pop, - dtype_t g, - Py_ssize_t max_bin, - Py_ssize_t mid_bin, double p0, - double p1, Py_ssize_t s0, - Py_ssize_t s1): +cdef inline void _kernel_subtract_mean(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): cdef Py_ssize_t i, sum, mean, n @@ -133,19 +137,20 @@ cdef inline double _kernel_subtract_mean(Py_ssize_t* histo, double pop, n += histo[i] mean += histo[i] * i if n > 0: - return (g - (mean / n)) * .5 + mid_bin + out[0] = ((g - (mean / n)) * .5 + mid_bin) else: - return 0 + out[0] = 0 else: - return 0 + out[0] = 0 -cdef inline double _kernel_enhance_contrast(Py_ssize_t* histo, double pop, - dtype_t g, - Py_ssize_t max_bin, - Py_ssize_t mid_bin, double p0, - double p1, Py_ssize_t s0, - Py_ssize_t s1): +cdef inline void _kernel_enhance_contrast(dtype_t_out[:] out, + Py_ssize_t* histo, double pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): cdef Py_ssize_t i, imin, imax, sum, delta @@ -164,21 +169,22 @@ cdef inline double _kernel_enhance_contrast(Py_ssize_t* histo, double pop, imax = i break if g > imax: - return imax + out[0] = imax if g < imin: - return imin + out[0] = imin if imax - g < g - imin: - return imax + out[0] = imax else: - return imin + out[0] = imin else: - return 0 + out[0] = 0 -cdef inline double _kernel_percentile(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_percentile(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t sum = 0 @@ -193,15 +199,16 @@ cdef inline double _kernel_percentile(Py_ssize_t* histo, double pop, dtype_t g, sum += histo[i] if sum > p0 * pop: break - return i + out[0] = i else: - return 0 + out[0] = 0 -cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_pop(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, sum, n @@ -212,15 +219,16 @@ cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, sum += histo[i] if (sum >= p0 * pop) and (sum <= p1 * pop): n += histo[i] - return n + out[0] = n else: - return 0 + out[0] = 0 -cdef inline double _kernel_threshold(Py_ssize_t* histo, double pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - double p0, double p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline void _kernel_threshold(dtype_t_out[:] out, Py_ssize_t* histo, + double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef int i cdef Py_ssize_t sum = 0 @@ -231,103 +239,103 @@ cdef inline double _kernel_threshold(Py_ssize_t* histo, double pop, dtype_t g, if sum >= p0 * pop: break - return (max_bin - 1) * (g >= i) + out[0] = ((max_bin - 1) * (g >= i)) else: - return 0 + out[0] = 0 def _autolevel(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, double p0, double p1, Py_ssize_t max_bin): - _core(_kernel_autolevel[dtype_t], image, selem, mask, out, + _core(_kernel_autolevel[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, p0, p1, 0, 0, max_bin) def _gradient(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, double p0, double p1, Py_ssize_t max_bin): - _core(_kernel_gradient[dtype_t], image, selem, mask, out, + _core(_kernel_gradient[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, p0, p1, 0, 0, max_bin) def _mean(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, double p0, double p1, Py_ssize_t max_bin): - _core(_kernel_mean[dtype_t], image, selem, mask, out, + _core(_kernel_mean[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, p0, p1, 0, 0, max_bin) def _sum(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, double p0, double p1, Py_ssize_t max_bin): - _core(_kernel_sum[dtype_t], image, selem, mask, out, + _core(_kernel_sum[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, p0, p1, 0, 0, max_bin) def _subtract_mean(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, double p0, double p1, Py_ssize_t max_bin): - _core(_kernel_subtract_mean[dtype_t], image, selem, mask, + _core(_kernel_subtract_mean[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, p0, p1, 0, 0, max_bin) def _enhance_contrast(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, double p0, double p1, Py_ssize_t max_bin): - _core(_kernel_enhance_contrast[dtype_t], image, selem, mask, + _core(_kernel_enhance_contrast[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, p0, p1, 0, 0, max_bin) def _percentile(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, double p0, double p1, Py_ssize_t max_bin): - _core(_kernel_percentile[dtype_t], image, selem, mask, out, + _core(_kernel_percentile[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, p0, 1, 0, 0, max_bin) def _pop(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, double p0, double p1, Py_ssize_t max_bin): - _core(_kernel_pop[dtype_t], image, selem, mask, out, + _core(_kernel_pop[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, p0, p1, 0, 0, max_bin) def _threshold(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t_out[:, ::1] out, + dtype_t_out[:, :, ::1] out, char shift_x, char shift_y, double p0, double p1, Py_ssize_t max_bin): - _core(_kernel_threshold[dtype_t], image, selem, mask, out, + _core(_kernel_threshold[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, p0, 1, 0, 0, max_bin) From 7ebb2388d29e8fca1a1af4d0ddfea6c4ff00b3ca Mon Sep 17 00:00:00 2001 From: Geoffrey French Date: Sun, 31 Aug 2014 16:22:35 +0100 Subject: [PATCH 0298/1122] Fixed some test failures and added a test for windowed_histogram. --- skimage/filter/rank/generic.py | 6 +++--- skimage/filter/rank/tests/test_rank.py | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index 07c592af..487d34b5 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -42,6 +42,9 @@ def _handle_input(image, selem, out, mask, out_dtype=None, pixel_size=1): mask = img_as_ubyte(mask) mask = np.ascontiguousarray(mask) + if image is out: + raise NotImplementedError("Cannot perform rank operation in place.") + if out is None: if out_dtype is None: out_dtype = image.dtype @@ -50,9 +53,6 @@ def _handle_input(image, selem, out, mask, out_dtype=None, pixel_size=1): if len(out.shape) == 2: out = out.reshape(out.shape+(pixel_size,)) - if image is out: - raise NotImplementedError("Cannot perform rank operation in place.") - is_8bit = image.dtype in (np.uint8, np.int8) if is_8bit: diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py index 5cbffd90..390a7790 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filter/rank/tests/test_rank.py @@ -542,6 +542,32 @@ def test_sum(): rank.sum_bilateral(image=image16, selem=elem, out=out16, mask=mask,s0=1000,s1=1000) assert_array_equal(r, out16) +def test_windowed_histogram(): + # check the number of valid pixels in the neighborhood + + image8 = np.array([[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]], dtype=np.uint8) + elem = np.ones((3, 3), dtype=np.uint8) + out8 = np.empty(image8.shape+(2,), dtype=np.uint8) + mask = np.ones(image8.shape, dtype=np.uint8) + + r0 = np.array([[3, 4, 3, 4, 3], + [4, 5, 3, 5, 4], + [3, 3, 0, 3, 3], + [4, 5, 3, 5, 4], + [3, 4, 3, 4, 3]], dtype=np.uint8) + r1 = np.array([[1, 2, 3, 2, 1], + [2, 4, 6, 4, 2], + [3, 6, 9, 6, 3], + [2, 4, 6, 4, 2], + [1, 2, 3, 2, 1]], dtype=np.uint8) + rank.windowed_histogram(image=image8, selem=elem, out=out8, mask=mask) + assert_array_equal(r0, out8[:,:,0]) + assert_array_equal(r1, out8[:,:,1]) + if __name__ == "__main__": run_module_suite() From e8987b582fded8f6e75084c080eeba2d423a0a14 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 31 Aug 2014 10:56:41 -0500 Subject: [PATCH 0299/1122] Fix matplotlib install, make installs verbose, add spacers --- .travis.yml | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index a2c99f97..c81bb2e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,7 @@ virtualenv: before_install: - export DISPLAY=:99.0 + - export SPACER = "\n\n\n\n\n\n\n\n\n\n*************************\n\n" - sh -e /etc/init.d/xvfb start - sudo apt-get update @@ -57,9 +58,13 @@ script: # Run all tests with minimum dependencies - nosetests --exe -v skimage + - echo -e $SPACER + # Run pep8 and flake tests - flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples + - echo -e $SPACER + # Install optional dependencies to get full test coverage # Notes: # - pyfits and imread do NOT support py3.2 @@ -68,22 +73,22 @@ script: # The solution was suggested here # https://groups.google.com/a/continuum.io/forum/#!topic/anaconda/-DLG2ZdTkw0 - if [[ $ENV == python=3.2 ]]; then - sudo apt-get install -qq python3-pyqt4; - travis_retry pip install -q matplotlib; - travis_retry pip install -q networkx; + sudo apt-get install python3-pyqt4; + travis_retry pip install matplotlib==1.3.1; + travis_retry pip install networkx; else - travis_retry conda install -q matplotlib pyqt networkx; + travis_retry conda install matplotlib pyqt networkx; sudo cp ~/miniconda/envs/test/include/png* /usr/include; rm ~/miniconda/envs/test/lib/libm-2.5.so; rm ~/miniconda/envs/test/lib/libm.*; - sudo apt-get install -qq libtiff4-dev libwebp-dev xcftools; - travis_retry pip install -q imread; - travis_retry pip install -q pyfits; + sudo apt-get install libtiff4-dev libwebp-dev xcftools; + travis_retry pip install imread; + travis_retry pip install pyfits; fi - - sudo apt-get install -qq libfreeimage3 + - sudo apt-get install libfreeimage3 # TODO: update when SimpleITK become available on py34 or hopefully pip - if [[ $ENV != python=3.4* ]]; then - travis_retry easy_install -q SimpleITK; + travis_retry easy_install SimpleITK; fi # Matplotlib settings @@ -93,11 +98,15 @@ script: - "echo 'backend : Agg' > $MPL_DIR/matplotlibrc" - "echo 'backend.qt4 : PyQt4' >> $MPL_DIR/matplotlibrc" + - echo -e $SPACER + # Run all doc examples - export PYTHONPATH=$(pwd):$PYTHONPATH - for f in doc/examples/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done - for f in doc/examples/applications/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done + - echo -e $SPACER + # run tests again with optional dependencies to get more coverage # measure coverage on py3.3 - if [[ $ENV == python=3.3* ]]; then From e8e23fa1544001a3885fcf23420e98683a841808 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 31 Aug 2014 11:03:30 -0500 Subject: [PATCH 0300/1122] Fix typo in spacer declaration --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c81bb2e9..8c6c9763 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ virtualenv: before_install: - export DISPLAY=:99.0 - - export SPACER = "\n\n\n\n\n\n\n\n\n\n*************************\n\n" + - export SPACER="\n\n\n\n\n\n\n\n\n\n*************************\n\n" - sh -e /etc/init.d/xvfb start - sudo apt-get update From da93619e59d3aac0ec4945ca5d4646beea770009 Mon Sep 17 00:00:00 2001 From: Geoffrey French Date: Sun, 31 Aug 2014 22:25:45 +0100 Subject: [PATCH 0301/1122] Fixed some issues with the new windowed_histogram function in filter.rank. It used to be able to output uint8 histogram, whose max pixel counts of 255 could easily overflow. Addressed by limiting the output type to float. Normalized histograms are now generated, otherwise behaviour is unpredictable at boundaries or when pixels are not permitted by a mask. The new optional n_bins parameter allows the caller to specify the size of the histogram generated. Having it fixed to image.max()+1 could result in feature vectors being shorter than desired just due to a value not being used in an image. An example has been added to the docs, that demonstrates the application of windows histograms in object matching; a single coin is extracted and found by chi squared histogram matching. --- doc/examples/plot_windowed_histogram.py | 129 ++++++++++++++++++++++++ skimage/filter/rank/generic.py | 31 ++++-- skimage/filter/rank/generic_cy.pyx | 10 +- skimage/filter/rank/tests/test_rank.py | 37 ++++--- 4 files changed, 182 insertions(+), 25 deletions(-) create mode 100644 doc/examples/plot_windowed_histogram.py diff --git a/doc/examples/plot_windowed_histogram.py b/doc/examples/plot_windowed_histogram.py new file mode 100644 index 00000000..7b0bcb3a --- /dev/null +++ b/doc/examples/plot_windowed_histogram.py @@ -0,0 +1,129 @@ +""" +======================== +Sliding window histogram +======================== + +This example extracts a single coin from the coins image and generates a +histogram of its greyscale values. + +It then computes a sliding window histogram of the complete image using +rank.windowed_histogram. The local histogram for the region surrounding +each pixel in the image is compared to that of the single coin, with +a similarity measure being computed and displayed. + +To demonstrate the rotational invariance of the technique, the same +test is performed on a version of the coins image rotated by 45 degrees. +""" +import numpy as np +import matplotlib +import matplotlib.pyplot as plt + +from skimage import data +from skimage.util.dtype import dtype_range +from skimage.util import img_as_ubyte +from skimage import exposure +from skimage.morphology import disk +from skimage.filter import rank +from skimage import transform + + +matplotlib.rcParams['font.size'] = 9 + + +def windowed_histogram_similarity(image, selem, reference_hist, n_bins): + # Compute normalized windowed histogram feature vector for each pixel + px_histograms = rank.windowed_histogram(image, selem, n_bins=n_bins) + + # Reshape coin histogram to (1,1,N) for broadcast when we want to use it in + # arithmetic operations with the windowed histograms fro the image + reference_hist = reference_hist.reshape((1,1) + reference_hist.shape) + + # Compute Chi squared distance metric: sum((X-Y)**2 / (X+Y); + # a measure of distance between histograms + X = px_histograms + Y = reference_hist + num = (X-Y)*(X-Y) + denom = X+Y + frac = num / denom + frac[denom==0] = 0 + chi_sqr = np.sum(frac, axis=2) * 0.5 + + # Generate a similarity measure. It needs to be low when distance is high. + # and high when distance is low; taking the reciprocal will do this. + # Chi squared will always be >= 0. Add small value to prevent divide by 0. + # Square the denominator to push low values toward 0; this makes the + # high similarity regions stand out in the figure created below; this + # us just done for aesthetics. + similarity = 1 / (chi_sqr + 1.0e-6)**2 + + return similarity + + +# Load the coins image +img = img_as_ubyte(data.coins()) +# img = img_as_ubyte(plt.imread('../../skimage/data/coins.png')) + +# Quantize to 16 levels of grayscale; this way the output image will have a +# 16-dimensional feature vector per pixel +quantized_img = img/16 + +# Select the coin from the 4th column, second row. +# Co-ordinate ordering: [x1,y1,x2,y2] +coin_coords = [184,100,228,148] # 44 x 44 region +coin = quantized_img[coin_coords[1]:coin_coords[3], coin_coords[0]:coin_coords[2]] + +# Compute coin histogram and normalize +coin_hist, _ = np.histogram(coin.flatten(), bins=16, range=(0,16)) +coin_hist = coin_hist.astype(float) / np.sum(coin_hist) + + +# Compute a disk shaped mask that will define the shape of our sliding window +# Example coin is ~44px across, so make a disk 61px wide (2*rad+1) to be big +# enough for other coins too. +selem = disk(30) + + +# Compute the similarity across the complete image +similarity = windowed_histogram_similarity(quantized_img, selem, coin_hist, + coin_hist.shape[0]) + +# Now try a rotated image +rotated_img = img_as_ubyte(transform.rotate(img, 45.0, resize=True)) +# Quantize to 16 levels as before +quantized_rotated_image = rotated_img/16 +# Similarity on rotated image +rotated_similarity = windowed_histogram_similarity(quantized_rotated_image, + selem, coin_hist, + coin_hist.shape[0]) + + + +# Plot it all +fig, axes = plt.subplots(nrows=5, figsize=(6, 18)) +ax0, ax1, ax2, ax3, ax4 = axes + +ax0.imshow(img, cmap='gray') +ax0.set_title('Original image') +ax0.axis('off') + +ax1.imshow(quantized_img, cmap='gray') +ax1.set_title('Quantized image') +ax1.axis('off') + +ax2.imshow(coin, cmap='gray') +ax2.set_title('Coin from 2nd row, 4th column') +ax2.axis('off') + +ax3.imshow(img, cmap='gray') +# While jet is not a great colormap, it makes the high similarity areas +# stand out +ax3.imshow(similarity, cmap='jet', alpha=0.5) +ax3.set_title('Original image with overlayed similarity') +ax3.axis('off') + +ax4.imshow(rotated_img, cmap='gray') +ax4.imshow(rotated_similarity, cmap='jet', alpha=0.5) +ax4.set_title('Rotated image with overlayed similarity') +ax4.axis('off') + +plt.show() diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index 487d34b5..2af2428e 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -868,8 +868,8 @@ def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): mask=mask, shift_x=shift_x, shift_y=shift_y) -def windowed_histogram(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Sliding window histogram +def windowed_histogram(image, selem, out=None, mask=None, shift_x=False, shift_y=False, n_bins=None): + """Normalized sliding window histogram Parameters ---------- @@ -886,15 +886,19 @@ def windowed_histogram(image, selem, out=None, mask=None, shift_x=False, shift_y Offset added to the structuring element center point. Shift is bounded to the structuring element sizes (center must be inside the given structuring element). + n_bins : int or None + The number of histogram bins. Will default to image.max() + 1 if None + is passed. Returns ------- - out : 3-D array (same dtype as input image) whose extra dimension - Output image. - - References - ---------- - .. [otsu] http://en.wikipedia.org/wiki/Otsu's_method + out : 3-D array with float dtype of dimensions (H,W,N), where (H,W) are + the dimensions of the input image and N is n_bins or image.max()+1 + if no value is provided as a parameter. Effectively, each pixel + is an N-dimensional feature vector that is the histogram. + The sum of the elements in the feature vector will be 1, unless + no pixels in the window were covered by both selem and mask, in which + case all elements will be 0. Examples -------- @@ -903,9 +907,14 @@ def windowed_histogram(image, selem, out=None, mask=None, shift_x=False, shift_y >>> from skimage.morphology import disk >>> img = data.camera() >>> hist_img = windowed_histogram(img, disk(5)) - >>> thresh_image = img >= local_otsu """ - return _apply_vector_per_pixel(generic_cy._windowed_hist, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y, pixel_size=image.max()+1) + if n_bins is None: + n_bins = image.max() + 1 + + return _apply_vector_per_pixel(generic_cy._windowed_hist, image, selem, + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y, + out_dtype=np.double, + pixel_size=n_bins) diff --git a/skimage/filter/rank/generic_cy.pyx b/skimage/filter/rank/generic_cy.pyx index 88766f44..bafd2432 100644 --- a/skimage/filter/rank/generic_cy.pyx +++ b/skimage/filter/rank/generic_cy.pyx @@ -378,8 +378,14 @@ cdef inline void _kernel_win_hist(dtype_t_out[:] out, Py_ssize_t* histo, Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t max_i - for i in xrange(out.shape[0]): - out[i] = histo[i] + cdef double scale + if pop: + scale = 1.0 / pop + for i in xrange(out.shape[0]): + out[i] = (histo[i] * scale) + else: + for i in xrange(out.shape[0]): + out[i] = 0 diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py index 390a7790..48d02462 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filter/rank/tests/test_rank.py @@ -551,22 +551,35 @@ def test_windowed_histogram(): [0, 1, 1, 1, 0], [0, 0, 0, 0, 0]], dtype=np.uint8) elem = np.ones((3, 3), dtype=np.uint8) - out8 = np.empty(image8.shape+(2,), dtype=np.uint8) + outf = np.empty(image8.shape+(2,), dtype=float) mask = np.ones(image8.shape, dtype=np.uint8) + # Population so we can normalize the expected output while maintaining + # code readability + pop = np.array([[4, 6, 6, 6, 4], + [6, 9, 9, 9, 6], + [6, 9, 9, 9, 6], + [6, 9, 9, 9, 6], + [4, 6, 6, 6, 4]], dtype=float) + r0 = np.array([[3, 4, 3, 4, 3], - [4, 5, 3, 5, 4], - [3, 3, 0, 3, 3], - [4, 5, 3, 5, 4], - [3, 4, 3, 4, 3]], dtype=np.uint8) + [4, 5, 3, 5, 4], + [3, 3, 0, 3, 3], + [4, 5, 3, 5, 4], + [3, 4, 3, 4, 3]], dtype=float) / pop r1 = np.array([[1, 2, 3, 2, 1], - [2, 4, 6, 4, 2], - [3, 6, 9, 6, 3], - [2, 4, 6, 4, 2], - [1, 2, 3, 2, 1]], dtype=np.uint8) - rank.windowed_histogram(image=image8, selem=elem, out=out8, mask=mask) - assert_array_equal(r0, out8[:,:,0]) - assert_array_equal(r1, out8[:,:,1]) + [2, 4, 6, 4, 2], + [3, 6, 9, 6, 3], + [2, 4, 6, 4, 2], + [1, 2, 3, 2, 1]], dtype=float) / pop + rank.windowed_histogram(image=image8, selem=elem, out=outf, mask=mask) + assert_array_equal(r0, outf[:,:,0]) + assert_array_equal(r1, outf[:,:,1]) + + # Test n_bins parameter + larger_output = rank.windowed_histogram(image=image8, selem=elem, + mask=mask, n_bins=5) + assert larger_output.shape[2] == 5 if __name__ == "__main__": From 2355c17e3cd7ef47d34090df6963d36d23e3787c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 24 Aug 2014 21:53:17 -0500 Subject: [PATCH 0302/1122] Add mask option to exposure.equalize_hist with test. Remove print and fix comparision to None Update test to make it clear there is a difference Fix mask logic and `equalize_hist` in `__all__`. Fix handling of mask and tweak mask test Another run at travis Update travis to avoid conflict --- .travis.yml | 2 +- skimage/exposure/exposure.py | 13 ++++++++++--- skimage/exposure/tests/test_exposure.py | 13 +++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8c6c9763..7f31692d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -73,7 +73,7 @@ script: # The solution was suggested here # https://groups.google.com/a/continuum.io/forum/#!topic/anaconda/-DLG2ZdTkw0 - if [[ $ENV == python=3.2 ]]; then - sudo apt-get install python3-pyqt4; + travis_retry sudo apt-get install python3-pyqt4; travis_retry pip install matplotlib==1.3.1; travis_retry pip install networkx; else diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index 3a2accbc..40ed3d10 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -5,7 +5,7 @@ from skimage import img_as_float from skimage.util.dtype import dtype_range, dtype_limits -__all__ = ['histogram', 'cumulative_distribution', 'equalize', +__all__ = ['histogram', 'cumulative_distribution', 'equalize_hist', 'rescale_intensity', 'adjust_gamma', 'adjust_log', 'adjust_sigmoid'] @@ -104,7 +104,7 @@ def cumulative_distribution(image, nbins=256): return img_cdf, bin_centers -def equalize_hist(image, nbins=256): +def equalize_hist(image, nbins=256, mask=None): """Return image after histogram equalization. Parameters @@ -113,6 +113,9 @@ def equalize_hist(image, nbins=256): Image array. nbins : int Number of bins for image histogram. + mask: ndarray of bools or 0s and 1s, optional + Array of same shape as `image`. Only points at which mask == True + are used for the equalization, which is applied to the whole image. Returns ------- @@ -130,7 +133,11 @@ def equalize_hist(image, nbins=256): """ image = img_as_float(image) - cdf, bin_centers = cumulative_distribution(image, nbins) + if mask is not None: + mask = np.array(mask, dtype=bool) + cdf, bin_centers = cumulative_distribution(image[mask], nbins) + else: + cdf, bin_centers = cumulative_distribution(image, nbins) out = np.interp(image.flat, bin_centers, cdf) return out.reshape(image.shape) diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py index 72a4e413..5361a221 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -38,6 +38,19 @@ def test_equalize_float(): check_cdf_slope(cdf) +def test_equalize_masked(): + img = skimage.img_as_float(test_img) + mask = np.zeros(test_img.shape) + mask[50:150, 50:250] = 1 + img_mask_eq = exposure.equalize_hist(img, mask=mask) + img_eq = exposure.equalize_hist(img) + + cdf, bin_edges = exposure.cumulative_distribution(img_mask_eq) + check_cdf_slope(cdf) + + assert not (img_eq == img_mask_eq).all() + + def check_cdf_slope(cdf): """Slope of cdf which should equal 1 for an equalized histogram.""" norm_intensity = np.linspace(0, 1, len(cdf)) From cdf7ad12bcd196ccadfb75836e4be009bfe1752d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 8 Aug 2014 20:08:06 -0400 Subject: [PATCH 0303/1122] Speedup moments_central function --- skimage/measure/_moments.pyx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/skimage/measure/_moments.pyx b/skimage/measure/_moments.pyx index e6ccdb75..8cb5ae47 100644 --- a/skimage/measure/_moments.pyx +++ b/skimage/measure/_moments.pyx @@ -79,11 +79,15 @@ def moments_central(double[:, :] image, double cr, double cc, """ cdef Py_ssize_t p, q, r, c cdef double[:, ::1] mu = np.zeros((order + 1, order + 1), dtype=np.double) - for p in range(order + 1): - for q in range(order + 1): - for r in range(image.shape[0]): - for c in range(image.shape[1]): - mu[p, q] += image[r, c] * (r - cr) ** q * (c - cc) ** p + cdef double val, dr, dc + for r in range(image.shape[0]): + dr = r - cr + for c in range(image.shape[1]): + dc = c - cc + val = image[r, c] + for p in range(order + 1): + for q in range(order + 1): + mu[p, q] += val * dr ** q * dc ** p return np.asarray(mu) From 8d2fdaa8f56440d095824fa3f9dc43305bbcca14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 8 Aug 2014 20:11:04 -0400 Subject: [PATCH 0304/1122] Speedup moments function --- skimage/measure/_moments.pyx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/skimage/measure/_moments.pyx b/skimage/measure/_moments.pyx index 8cb5ae47..f5bb63af 100644 --- a/skimage/measure/_moments.pyx +++ b/skimage/measure/_moments.pyx @@ -39,7 +39,16 @@ def moments(double[:, :] image, Py_ssize_t order=3): .. [4] http://en.wikipedia.org/wiki/Image_moment """ - return moments_central(image, 0, 0, order) + cdef Py_ssize_t p, q, r, c + cdef double[:, ::1] mu = np.zeros((order + 1, order + 1), dtype=np.double) + cdef double val + for r in range(image.shape[0]): + for c in range(image.shape[1]): + val = image[r, c] + for p in range(order + 1): + for q in range(order + 1): + mu[p, q] += val * r ** q * c ** p + return np.asarray(mu) def moments_central(double[:, :] image, double cr, double cc, From 4f1adee2ecf76a326a8596b3a8e9ea1a3aeace16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 30 Aug 2014 11:41:32 -0400 Subject: [PATCH 0305/1122] Make moments functions C functions and reuse centralized moments --- skimage/measure/_moments.pyx | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/skimage/measure/_moments.pyx b/skimage/measure/_moments.pyx index f5bb63af..8fdb30c9 100644 --- a/skimage/measure/_moments.pyx +++ b/skimage/measure/_moments.pyx @@ -5,7 +5,7 @@ import numpy as np -def moments(double[:, :] image, Py_ssize_t order=3): +cpdef moments(double[:, :] image, Py_ssize_t order=3): """Calculate all raw image moments up to a certain order. The following properties can be calculated from raw image moments: @@ -39,20 +39,11 @@ def moments(double[:, :] image, Py_ssize_t order=3): .. [4] http://en.wikipedia.org/wiki/Image_moment """ - cdef Py_ssize_t p, q, r, c - cdef double[:, ::1] mu = np.zeros((order + 1, order + 1), dtype=np.double) - cdef double val - for r in range(image.shape[0]): - for c in range(image.shape[1]): - val = image[r, c] - for p in range(order + 1): - for q in range(order + 1): - mu[p, q] += val * r ** q * c ** p - return np.asarray(mu) + return moments_central(image, 0, 0, order) -def moments_central(double[:, :] image, double cr, double cc, - Py_ssize_t order=3): +cpdef moments_central(double[:, :] image, double cr, double cc, + Py_ssize_t order=3): """Calculate all central image moments up to a certain order. Note that central moments are translation invariant but not scale and @@ -88,15 +79,16 @@ def moments_central(double[:, :] image, double cr, double cc, """ cdef Py_ssize_t p, q, r, c cdef double[:, ::1] mu = np.zeros((order + 1, order + 1), dtype=np.double) - cdef double val, dr, dc + cdef double val, dr, dc, dcp for r in range(image.shape[0]): dr = r - cr for c in range(image.shape[1]): dc = c - cc val = image[r, c] for p in range(order + 1): + dcp = dc ** p for q in range(order + 1): - mu[p, q] += val * dr ** q * dc ** p + mu[p, q] += val * dr ** q * dcp return np.asarray(mu) From 23083636fe44b28bf1cb00350800e3ec86ad3a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 30 Aug 2014 11:46:22 -0400 Subject: [PATCH 0306/1122] Replace pow() with iterative multiplication --- skimage/measure/_moments.pyx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/skimage/measure/_moments.pyx b/skimage/measure/_moments.pyx index 8fdb30c9..5a9bc11a 100644 --- a/skimage/measure/_moments.pyx +++ b/skimage/measure/_moments.pyx @@ -79,16 +79,19 @@ cpdef moments_central(double[:, :] image, double cr, double cc, """ cdef Py_ssize_t p, q, r, c cdef double[:, ::1] mu = np.zeros((order + 1, order + 1), dtype=np.double) - cdef double val, dr, dc, dcp + cdef double val, dr, dc, dcp, drq for r in range(image.shape[0]): dr = r - cr for c in range(image.shape[1]): dc = c - cc val = image[r, c] + dcp = 1 for p in range(order + 1): - dcp = dc ** p + drq = 1 for q in range(order + 1): - mu[p, q] += val * dr ** q * dcp + mu[p, q] += val * drq * dcp + drq *= dr + dcp *= dc return np.asarray(mu) From d9a85a167e9d824937c46a85b332263ae91316c8 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sat, 12 Jul 2014 20:32:20 -0500 Subject: [PATCH 0307/1122] Move handling of blit behavior to axes object. --- skimage/viewer/canvastools/base.py | 58 +++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/skimage/viewer/canvastools/base.py b/skimage/viewer/canvastools/base.py index 29e6099d..284e2bd6 100644 --- a/skimage/viewer/canvastools/base.py +++ b/skimage/viewer/canvastools/base.py @@ -11,6 +11,31 @@ def _pass(*args): pass +class BlitManager(object): + """Object that manages blits on an axes""" + def __init__(self, ax): + self.ax = ax + self.canvas = ax.figure.canvas + self.canvas.mpl_connect('draw_event', self.on_draw_event) + self.ax = ax + self.background = None + self.artists = [] + + def on_draw_event(self, event=None): + self.background = self.canvas.copy_from_bbox(self.ax.bbox) + self.draw_artists() + + def redraw(self): + if self.background is not None: + self.canvas.restore_region(self.background) + self.draw_artists() + self.canvas.blit(self.ax.bbox) + + def draw_artists(self): + for artist in self.artists: + self.ax.draw_artist(artist) + + class CanvasToolBase(object): """Base canvas tool for matplotlib axes. @@ -28,26 +53,31 @@ class CanvasToolBase(object): useblit : bool If True, update canvas by blitting, which is much faster than normal redrawing (turn off for debugging purposes). + nograb_draw : bool + If a mouse click is detected, but it does not grab a handle, + redraw the line from scratch. True by default. """ + def __init__(self, ax, on_move=None, on_enter=None, on_release=None, useblit=True): self.ax = ax self.canvas = ax.figure.canvas - self.img_background = None self.cids = [] self._artists = [] self.active = True + self.connect_event('draw_event', self._on_draw_event) if useblit: - self.connect_event('draw_event', self._blit_on_draw_event) + if not hasattr(ax, 'blit_manager'): + ax.blit_manager = BlitManager(ax) + ax.blit_manager.artists.extend(self._artists) + self.useblit = useblit self.callback_on_move = _pass if on_move is None else on_move self.callback_on_enter = _pass if on_enter is None else on_enter self.callback_on_release = _pass if on_release is None else on_release - self.connect_event('key_press_event', self._on_key_press) - def connect_event(self, event, callback): """Connect callback with an event. @@ -74,9 +104,13 @@ class CanvasToolBase(object): for artist in self._artists: artist.set_visible(val) - def _blit_on_draw_event(self, event=None): - self.img_background = self.canvas.copy_from_bbox(self.ax.bbox) - self._draw_artists() + def _on_draw_event(self, event=None): + if self.useblit: + for artist in self._artists: + if not artist in self.ax.blit_manager.artists: + self.ax.blit_manager.artists.append(artist) + else: + self._draw_artists() def _draw_artists(self): for artist in self._artists: @@ -97,14 +131,12 @@ class CanvasToolBase(object): This method should be called by subclasses when artists are updated. """ - if self.useblit and self.img_background is not None: - self.canvas.restore_region(self.img_background) - self._draw_artists() - self.canvas.blit(self.ax.bbox) + if not self.useblit: + self.canvas.draw() else: - self.canvas.draw_idle() + self.ax.blit_manager.redraw() - def _on_key_press(self, event): + def on_key_press(self, event): if event.key == 'enter': self.callback_on_enter(self.geometry) self.set_visible(False) From 410f3926e4b7fce9645506054b5dfd1668502811 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sat, 12 Jul 2014 20:32:43 -0500 Subject: [PATCH 0308/1122] Push handling of line tools to axes level for better coordination. --- skimage/viewer/canvastools/linetool.py | 84 +++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 10 deletions(-) diff --git a/skimage/viewer/canvastools/linetool.py b/skimage/viewer/canvastools/linetool.py index 61d94825..4b53ebad 100644 --- a/skimage/viewer/canvastools/linetool.py +++ b/skimage/viewer/canvastools/linetool.py @@ -10,6 +10,63 @@ from skimage.viewer.canvastools.base import CanvasToolBase, ToolHandles __all__ = ['LineTool', 'ThickLineTool'] +class EventManager(object): + """Object that manages events on a canvas""" + def __init__(self, ax): + self.canvas = ax.figure.canvas + self.connect_event('button_press_event', self.on_mouse_press) + self.connect_event('key_press_event', self.on_key_press) + self.connect_event('button_release_event', self.on_mouse_release) + self.connect_event('motion_notify_event', self.on_move) + + self.tools = [] + self.active_tool = None + + def connect_event(self, name, handler): + self.canvas.mpl_connect(name, handler) + + def attach(self, tool): + self.tools.append(tool) + self.active_tool = tool + + def on_mouse_press(self, event): + for tool in self.tools: + if not tool.ignore(event) and tool.hit_test(event): + self.active_tool = tool + tool.on_mouse_press(event) + return + if self.active_tool and not self.active_tool.ignore(event): + self.active_tool.on_mouse_press(event) + return + for tool in self.tools: + if not tool.ignore(event): + self.active_tool = tool + tool.on_mouse_press(event) + return + + def on_key_press(self, event): + tool = self.get_tool() + if not tool is None and not tool.ignore(event): + tool.on_key_press(event) + + def get_tool(self): + if not self.tools: + return + if self.active_tool is None: + self.active_tool = self.tools[0] + return self.active_tool + + def on_mouse_release(self, event): + tool = self.get_tool() + if not tool is None and not tool.ignore(event): + tool.on_mouse_release(event) + + def on_move(self, event): + tool = self.get_tool() + if not tool is None and not tool.ignore(event): + tool.on_move(event) + + class LineTool(CanvasToolBase): """Widget for line selection in a plot. @@ -35,9 +92,10 @@ class LineTool(CanvasToolBase): End points of line ((x1, y1), (x2, y2)). """ def __init__(self, ax, on_move=None, on_release=None, on_enter=None, - maxdist=10, line_props=None): + maxdist=10, line_props=None, + **kwargs): super(LineTool, self).__init__(ax, on_move=on_move, on_enter=on_enter, - on_release=on_release) + on_release=on_release, **kwargs) props = dict(color='r', linewidth=1, alpha=0.4, solid_capstyle='butt') props.update(line_props if line_props is not None else {}) @@ -56,16 +114,16 @@ class LineTool(CanvasToolBase): self._handles.set_visible(False) self._artists = [self._line, self._handles.artist] + if not hasattr(ax, 'event_manager'): + ax.event_manager = EventManager(ax) + ax.event_manager.attach(self) + if on_enter is None: def on_enter(pts): x, y = np.transpose(pts) print("length = %0.2f" % np.sqrt(np.diff(x)**2 + np.diff(y)**2)) self.callback_on_enter = on_enter - self.connect_event('button_press_event', self.on_mouse_press) - self.connect_event('button_release_event', self.on_mouse_release) - self.connect_event('motion_notify_event', self.on_move) - @property def end_points(self): return self._end_pts.astype(int) @@ -76,19 +134,24 @@ class LineTool(CanvasToolBase): self._line.set_data(np.transpose(pts)) self._handles.set_data(np.transpose(pts)) - self._line.set_linewidth(self.linewidth) self.set_visible(True) self.redraw() - def on_mouse_press(self, event): + def hit_test(self, event): if event.button != 1 or not self.ax.in_axes(event): - return - self.set_visible(True) + return False idx, px_dist = self._handles.closest(event.x, event.y) if px_dist < self.maxdist: self._active_pt = idx + return True else: + self._active_pt = None + return False + + def on_mouse_press(self, event): + self.set_visible(True) + if self._active_pt is None: self._active_pt = 0 x, y = event.xdata, event.ydata self._end_pts = np.array([[x, y], [x, y]]) @@ -98,6 +161,7 @@ class LineTool(CanvasToolBase): return self._active_pt = None self.callback_on_release(self.geometry) + self.redraw() def on_move(self, event): if event.button != 1 or self._active_pt is None: From 1799846be9552c7d13586e5ad4b147a121329a87 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sat, 12 Jul 2014 20:44:21 -0500 Subject: [PATCH 0309/1122] Remove unused parameter from docstring --- skimage/viewer/canvastools/base.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/skimage/viewer/canvastools/base.py b/skimage/viewer/canvastools/base.py index 284e2bd6..fce163f3 100644 --- a/skimage/viewer/canvastools/base.py +++ b/skimage/viewer/canvastools/base.py @@ -53,9 +53,6 @@ class CanvasToolBase(object): useblit : bool If True, update canvas by blitting, which is much faster than normal redrawing (turn off for debugging purposes). - nograb_draw : bool - If a mouse click is detected, but it does not grab a handle, - redraw the line from scratch. True by default. """ def __init__(self, ax, on_move=None, on_enter=None, on_release=None, From 2d9d8bb94ce8f32ff8d07feab60a509c16d21cdb Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sun, 20 Jul 2014 18:39:49 -0500 Subject: [PATCH 0310/1122] Refactor blit manager and event manager to viewer --- skimage/viewer/canvastools/base.py | 115 +++++------------------- skimage/viewer/canvastools/linetool.py | 83 +++-------------- skimage/viewer/canvastools/painttool.py | 20 ++--- skimage/viewer/canvastools/recttool.py | 25 +++--- skimage/viewer/viewers/core.py | 102 ++++++++++++++++++++- 5 files changed, 153 insertions(+), 192 deletions(-) diff --git a/skimage/viewer/canvastools/base.py b/skimage/viewer/canvastools/base.py index fce163f3..a1a06360 100644 --- a/skimage/viewer/canvastools/base.py +++ b/skimage/viewer/canvastools/base.py @@ -11,38 +11,13 @@ def _pass(*args): pass -class BlitManager(object): - """Object that manages blits on an axes""" - def __init__(self, ax): - self.ax = ax - self.canvas = ax.figure.canvas - self.canvas.mpl_connect('draw_event', self.on_draw_event) - self.ax = ax - self.background = None - self.artists = [] - - def on_draw_event(self, event=None): - self.background = self.canvas.copy_from_bbox(self.ax.bbox) - self.draw_artists() - - def redraw(self): - if self.background is not None: - self.canvas.restore_region(self.background) - self.draw_artists() - self.canvas.blit(self.ax.bbox) - - def draw_artists(self): - for artist in self.artists: - self.ax.draw_artist(artist) - - class CanvasToolBase(object): """Base canvas tool for matplotlib axes. Parameters ---------- - ax : :class:`matplotlib.axes.Axes` - Matplotlib axes where tool is displayed. + viewer : :class:`skimage.viewer.Viewer` + Skimage viewer object. on_move : function Function called whenever a control handle is moved. This function must accept the end points of line as the only argument. @@ -50,45 +25,19 @@ class CanvasToolBase(object): Function called whenever the control handle is released. on_enter : function Function called whenever the "enter" key is pressed. - useblit : bool - If True, update canvas by blitting, which is much faster than normal - redrawing (turn off for debugging purposes). """ - def __init__(self, ax, on_move=None, on_enter=None, on_release=None, + def __init__(self, viewer, on_move=None, on_enter=None, on_release=None, useblit=True): - self.ax = ax - self.canvas = ax.figure.canvas - self.cids = [] - self._artists = [] + self.viewer = viewer + self.ax = viewer.ax + self.artists = [] self.active = True - self.connect_event('draw_event', self._on_draw_event) - if useblit: - if not hasattr(ax, 'blit_manager'): - ax.blit_manager = BlitManager(ax) - ax.blit_manager.artists.extend(self._artists) - - self.useblit = useblit - self.callback_on_move = _pass if on_move is None else on_move self.callback_on_enter = _pass if on_enter is None else on_enter self.callback_on_release = _pass if on_release is None else on_release - def connect_event(self, event, callback): - """Connect callback with an event. - - This should be used in lieu of `figure.canvas.mpl_connect` since this - function stores call back ids for later clean up. - """ - cid = self.canvas.mpl_connect(event, callback) - self.cids.append(cid) - - def disconnect_events(self): - """Disconnect all events created by this widget.""" - for c in self.cids: - self.canvas.mpl_disconnect(c) - def ignore(self, event): """Return True if event should be ignored. @@ -97,47 +46,30 @@ class CanvasToolBase(object): """ return not self.active + def redraw(self): + self.viewer.redraw() + def set_visible(self, val): for artist in self._artists: artist.set_visible(val) - def _on_draw_event(self, event=None): - if self.useblit: - for artist in self._artists: - if not artist in self.ax.blit_manager.artists: - self.ax.blit_manager.artists.append(artist) - else: - self._draw_artists() - - def _draw_artists(self): - for artist in self._artists: - self.ax.draw_artist(artist) - - def remove(self): - """Remove artists and events from axes. - - Note that the naming here mimics the interface of Matplotlib artists. - """ - #TODO: For some reason, RectangleTool doesn't get properly removed - self.disconnect_events() - for a in self._artists: - a.remove() - - def redraw(self): - """Redraw image and canvas artists. - - This method should be called by subclasses when artists are updated. - """ - if not self.useblit: - self.canvas.draw() - else: - self.ax.blit_manager.redraw() - def on_key_press(self, event): if event.key == 'enter': self.callback_on_enter(self.geometry) self.set_visible(False) - self.redraw() + self.viewer.redraw() + + def on_mouse_press(self, event): + pass + + def on_mouse_release(self, event): + pass + + def on_move(self, event): + pass + + def on_scroll(self, event): + pass @property def geometry(self): @@ -190,9 +122,6 @@ class ToolHandles(object): def set_animated(self, val): self._markers.set_animated(val) - def draw(self): - self.ax.draw_artist(self._markers) - def closest(self, x, y): """Return index and pixel distance to closest index.""" pts = np.transpose((self.x, self.y)) diff --git a/skimage/viewer/canvastools/linetool.py b/skimage/viewer/canvastools/linetool.py index 4b53ebad..e1b48c05 100644 --- a/skimage/viewer/canvastools/linetool.py +++ b/skimage/viewer/canvastools/linetool.py @@ -10,70 +10,13 @@ from skimage.viewer.canvastools.base import CanvasToolBase, ToolHandles __all__ = ['LineTool', 'ThickLineTool'] -class EventManager(object): - """Object that manages events on a canvas""" - def __init__(self, ax): - self.canvas = ax.figure.canvas - self.connect_event('button_press_event', self.on_mouse_press) - self.connect_event('key_press_event', self.on_key_press) - self.connect_event('button_release_event', self.on_mouse_release) - self.connect_event('motion_notify_event', self.on_move) - - self.tools = [] - self.active_tool = None - - def connect_event(self, name, handler): - self.canvas.mpl_connect(name, handler) - - def attach(self, tool): - self.tools.append(tool) - self.active_tool = tool - - def on_mouse_press(self, event): - for tool in self.tools: - if not tool.ignore(event) and tool.hit_test(event): - self.active_tool = tool - tool.on_mouse_press(event) - return - if self.active_tool and not self.active_tool.ignore(event): - self.active_tool.on_mouse_press(event) - return - for tool in self.tools: - if not tool.ignore(event): - self.active_tool = tool - tool.on_mouse_press(event) - return - - def on_key_press(self, event): - tool = self.get_tool() - if not tool is None and not tool.ignore(event): - tool.on_key_press(event) - - def get_tool(self): - if not self.tools: - return - if self.active_tool is None: - self.active_tool = self.tools[0] - return self.active_tool - - def on_mouse_release(self, event): - tool = self.get_tool() - if not tool is None and not tool.ignore(event): - tool.on_mouse_release(event) - - def on_move(self, event): - tool = self.get_tool() - if not tool is None and not tool.ignore(event): - tool.on_move(event) - - class LineTool(CanvasToolBase): """Widget for line selection in a plot. Parameters ---------- - ax : :class:`matplotlib.axes.Axes` - Matplotlib axes where tool is displayed. + viewer : :class:`skimage.viewer.Viewer` + Skimage viewer object. on_move : function Function called whenever a control handle is moved. This function must accept the end points of line as the only argument. @@ -91,7 +34,7 @@ class LineTool(CanvasToolBase): end_points : 2D array End points of line ((x1, y1), (x2, y2)). """ - def __init__(self, ax, on_move=None, on_release=None, on_enter=None, + def __init__(self, viewer, on_move=None, on_release=None, on_enter=None, maxdist=10, line_props=None, **kwargs): super(LineTool, self).__init__(ax, on_move=on_move, on_enter=on_enter, @@ -108,21 +51,18 @@ class LineTool(CanvasToolBase): self._end_pts = np.transpose([x, y]) self._line = lines.Line2D(x, y, visible=False, animated=True, **props) - ax.add_line(self._line) + self.ax.add_line(self._line) - self._handles = ToolHandles(ax, x, y) + self._handles = ToolHandles(self.ax, x, y) self._handles.set_visible(False) - self._artists = [self._line, self._handles.artist] - - if not hasattr(ax, 'event_manager'): - ax.event_manager = EventManager(ax) - ax.event_manager.attach(self) + self.artists = [self._line, self._handles.artist] if on_enter is None: def on_enter(pts): x, y = np.transpose(pts) print("length = %0.2f" % np.sqrt(np.diff(x)**2 + np.diff(y)**2)) self.callback_on_enter = on_enter + viewer.add_tool(self) @property def end_points(self): @@ -189,8 +129,8 @@ class ThickLineTool(LineTool): Parameters ---------- - ax : :class:`matplotlib.axes.Axes` - Matplotlib axes where tool is displayed. + viewer : :class:`skimage.viewer.Viewer` + Skimage viewer object. on_move : function Function called whenever a control handle is moved. This function must accept the end points of line as the only argument. @@ -211,7 +151,7 @@ class ThickLineTool(LineTool): End points of line ((x1, y1), (x2, y2)). """ - def __init__(self, ax, on_move=None, on_enter=None, on_release=None, + def __init__(self, viewer, on_move=None, on_enter=None, on_release=None, on_change=None, maxdist=10, line_props=None): super(ThickLineTool, self).__init__(ax, on_move=on_move, @@ -225,9 +165,6 @@ class ThickLineTool(LineTool): pass self.callback_on_change = on_change - self.connect_event('scroll_event', self.on_scroll) - self.connect_event('key_press_event', self.on_key_press) - def on_scroll(self, event): if not event.inaxes: return diff --git a/skimage/viewer/canvastools/painttool.py b/skimage/viewer/canvastools/painttool.py index 9b94e0a2..8d3c9449 100644 --- a/skimage/viewer/canvastools/painttool.py +++ b/skimage/viewer/canvastools/painttool.py @@ -17,8 +17,8 @@ class PaintTool(CanvasToolBase): Parameters ---------- - ax : :class:`matplotlib.axes.Axes` - Matplotlib axes where tool is displayed. + viewer : :class:`skimage.viewer.Viewer` + Skimage viewer object. overlay_shape : shape tuple 2D shape tuple used to initialize overlay image. alpha : float (between [0, 1]) @@ -41,8 +41,9 @@ class PaintTool(CanvasToolBase): label : int Current paint color. """ - def __init__(self, ax, overlay_shape, radius=5, alpha=0.3, on_move=None, - on_release=None, on_enter=None, rect_props=None): + def __init__(self, viewer, overlay_shape, radius=5, alpha=0.3, + on_move=None, on_release=None, on_enter=None, + rect_props=None): super(PaintTool, self).__init__(ax, on_move=on_move, on_enter=on_enter, on_release=on_release) @@ -63,11 +64,8 @@ class PaintTool(CanvasToolBase): self.radius = radius # Note that the order is important: Redraw cursor *after* overlay - self._artists = [self._overlay_plot, self._cursor] - - self.connect_event('button_press_event', self.on_mouse_press) - self.connect_event('button_release_event', self.on_mouse_release) - self.connect_event('motion_notify_event', self.on_move) + self.artists = [self._overlay_plot, self._cursor] + view.add_tool(self) @property def label(self): @@ -123,7 +121,7 @@ class PaintTool(CanvasToolBase): self.radius = self._radius self.overlay = np.zeros(shape, dtype='uint8') - def _on_key_press(self, event): + def on_key_press(self, event): if event.key == 'enter': self.callback_on_enter(self.geometry) self.redraw() @@ -140,7 +138,7 @@ class PaintTool(CanvasToolBase): self.callback_on_release(self.geometry) def on_move(self, event): - if not self.ax.in_axes(event): + if not self.viewer.ax.in_axes(event): self._cursor.set_visible(False) self.redraw() # make sure cursor is not visible return diff --git a/skimage/viewer/canvastools/recttool.py b/skimage/viewer/canvastools/recttool.py index 929fe939..d3724bf9 100644 --- a/skimage/viewer/canvastools/recttool.py +++ b/skimage/viewer/canvastools/recttool.py @@ -18,8 +18,8 @@ class RectangleTool(CanvasToolBase, RectangleSelector): Parameters ---------- - ax : :class:`matplotlib.axes.Axes` - Matplotlib axes where tool is displayed. + viewer : :class:`skimage.viewer.Viewer` + Skimage viewer object. on_move : function Function called whenever a control handle is moved. This function must accept the rectangle extents as the only argument. @@ -39,7 +39,7 @@ class RectangleTool(CanvasToolBase, RectangleSelector): Rectangle extents: (xmin, xmax, ymin, ymax). """ - def __init__(self, ax, on_move=None, on_release=None, on_enter=None, + def __init__(self, viewer, on_move=None, on_release=None, on_enter=None, maxdist=10, rect_props=None): CanvasToolBase.__init__(self, ax, on_move=on_move, on_enter=on_enter, on_release=on_release) @@ -48,9 +48,9 @@ class RectangleTool(CanvasToolBase, RectangleSelector): props.update(rect_props if rect_props is not None else {}) if props['edgecolor'] is None: props['edgecolor'] = props['facecolor'] - RectangleSelector.__init__(self, ax, lambda *args: None, - rectprops=props, - useblit=self.useblit) + RectangleSelector.__init__(self, self.ax, lambda *args: None, + rectprops=props) + self.disconnect_events() # events are handled by the viewer # Alias rectangle attribute, which is initialized in RectangleSelector. self._rect = self.to_draw self._rect.set_animated(True) @@ -74,9 +74,10 @@ class RectangleTool(CanvasToolBase, RectangleSelector): self._edge_handles = ToolHandles(ax, xe, ye, marker='s', marker_props=props) - self._artists = [self._rect, - self._corner_handles.artist, - self._edge_handles.artist] + self.artists = [self._rect, + self._corner_handles.artist, + self._edge_handles.artist] + viewer.add_tool(self) @property def _rect_bbox(self): @@ -129,7 +130,7 @@ class RectangleTool(CanvasToolBase, RectangleSelector): self.set_visible(True) self.redraw() - def release(self, event): + def on_mouse_release(self, event): if event.button != 1: return if not self.ax.in_axes(event): @@ -142,7 +143,7 @@ class RectangleTool(CanvasToolBase, RectangleSelector): self.redraw() self.callback_on_release(self.geometry) - def press(self, event): + def on_mouse_press(self, event): if event.button != 1 or not self.ax.in_axes(event): return self._set_active_handle(event) @@ -177,7 +178,7 @@ class RectangleTool(CanvasToolBase, RectangleSelector): y1, y2 = y2, event.ydata self._extents_on_press = x1, x2, y1, y2 - def onmove(self, event): + def on_move(self, event): if self.eventpress is None or not self.ax.in_axes(event): return diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 52ac073a..afd3ff2b 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -12,7 +12,6 @@ else: from skimage import io, img_as_float from skimage.util.dtype import dtype_range from skimage.exposure import rescale_intensity -from skimage._shared.testing import doctest_skip_parser import numpy as np from .. import utils from ..widgets import Slider @@ -51,6 +50,88 @@ def mpl_image_to_rgba(mpl_image): return img_as_float(image) +class BlitManager(object): + """Object that manages blits on an axes""" + def __init__(self, ax): + self.ax = ax + self.canvas = ax.figure.canvas + self.canvas.mpl_connect('draw_event', self.on_draw_event) + self.ax = ax + self.background = None + self.artists = [] + + def on_draw_event(self, event=None): + self.background = self.canvas.copy_from_bbox(self.ax.bbox) + self.draw_artists() + + def redraw(self): + if self.background is not None: + self.canvas.restore_region(self.background) + self.draw_artists() + self.canvas.blit(self.ax.bbox) + + def draw_artists(self): + for artist in self.artists: + self.ax.draw_artist(artist) + + +class EventManager(object): + """Object that manages events on a canvas""" + def __init__(self, ax): + self.canvas = ax.figure.canvas + self.connect_event('button_press_event', self.on_mouse_press) + self.connect_event('key_press_event', self.on_key_press) + self.connect_event('button_release_event', self.on_mouse_release) + self.connect_event('motion_notify_event', self.on_move) + + self.tools = [] + self.active_tool = None + + def connect_event(self, name, handler): + self.canvas.mpl_connect(name, handler) + + def attach(self, tool): + self.tools.append(tool) + self.active_tool = tool + + def on_mouse_press(self, event): + for tool in self.tools: + if not tool.ignore(event) and tool.hit_test(event): + self.active_tool = tool + tool.on_mouse_press(event) + return + if self.active_tool and not self.active_tool.ignore(event): + self.active_tool.on_mouse_press(event) + return + for tool in reversed(self.tools): + if not tool.ignore(event): + self.active_tool = tool + tool.on_mouse_press(event) + return + + def on_key_press(self, event): + tool = self.get_tool() + if not tool is None and not tool.ignore(event): + tool.on_key_press(event) + + def get_tool(self): + if not self.tools: + return + if self.active_tool is None: + self.active_tool = self.tools[0] + return self.active_tool + + def on_mouse_release(self, event): + tool = self.get_tool() + if not tool is None and not tool.ignore(event): + tool.on_mouse_release(event) + + def on_move(self, event): + tool = self.get_tool() + if not tool is None and not tool.ignore(event): + tool.on_move(event) + + class ImageViewer(QtGui.QMainWindow): """Viewer for displaying images. @@ -91,7 +172,7 @@ class ImageViewer(QtGui.QMainWindow): # Signal that the original image has been changed original_image_changed = Signal(np.ndarray) - def __init__(self, image): + def __init__(self, image, useblit=True): # Start main loop utils.init_qtapp() super(ImageViewer, self).__init__() @@ -125,6 +206,12 @@ class ImageViewer(QtGui.QMainWindow): self.canvas.setParent(self) self.ax.autoscale(enable=False) + self._tools = [] + self.useblit = useblit + if useblit: + self._blit_manager = BlitManager(self.ax) + self._event_manager = EventManager(self.ax) + self._image_plot = self.ax.images[0] self._update_original_image(image) self.plugins = [] @@ -238,7 +325,10 @@ class ImageViewer(QtGui.QMainWindow): return [p.output() for p in self.plugins] def redraw(self): - self.canvas.draw_idle() + if self.useblit: + self._blit_manager.redraw() + else: + self.canvas.draw_idle() @property def image(self): @@ -280,6 +370,12 @@ class ImageViewer(QtGui.QMainWindow): else: self.status_message('') + def add_tool(self, tool): + if self.blit: + self._blit_manager.artists.extend(tool.artists) + self._tools.append(tool) + self._event_manager.attach(tool) + def _format_coord(self, x, y): # callback function to format coordinate display in status bar x = int(x + 0.5) From 1775e959d6bec18bf97ee3e42810e02aa4e2d06a Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sun, 20 Jul 2014 19:08:14 -0500 Subject: [PATCH 0311/1122] Switch plugins to use new tool api --- skimage/viewer/plugins/color_histogram.py | 3 ++- skimage/viewer/plugins/crop.py | 2 +- skimage/viewer/plugins/labelplugin.py | 2 +- skimage/viewer/plugins/lineprofile.py | 2 +- skimage/viewer/plugins/measure.py | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/skimage/viewer/plugins/color_histogram.py b/skimage/viewer/plugins/color_histogram.py index 4826fc44..99205c41 100644 --- a/skimage/viewer/plugins/color_histogram.py +++ b/skimage/viewer/plugins/color_histogram.py @@ -21,7 +21,8 @@ class ColorHistogram(PlotPlugin): def attach(self, image_viewer): super(ColorHistogram, self).attach(image_viewer) - self.rect_tool = RectangleTool(self.ax, on_release=self.ab_selected) + self.rect_tool = RectangleTool(image_viewer, + on_release=self.ab_selected) self._on_new_image(image_viewer.image) def _on_new_image(self, image): diff --git a/skimage/viewer/plugins/crop.py b/skimage/viewer/plugins/crop.py index 61f61034..29ad5866 100644 --- a/skimage/viewer/plugins/crop.py +++ b/skimage/viewer/plugins/crop.py @@ -18,7 +18,7 @@ class Crop(Plugin): def attach(self, image_viewer): super(Crop, self).attach(image_viewer) - self.rect_tool = RectangleTool(self.image_viewer.ax, + self.rect_tool = RectangleTool(image_viewer, maxdist=self.maxdist, on_enter=self.crop) self.artists.append(self.rect_tool) diff --git a/skimage/viewer/plugins/labelplugin.py b/skimage/viewer/plugins/labelplugin.py index 06ba2dd9..fa2b1940 100644 --- a/skimage/viewer/plugins/labelplugin.py +++ b/skimage/viewer/plugins/labelplugin.py @@ -37,7 +37,7 @@ class LabelPainter(Plugin): super(LabelPainter, self).attach(image_viewer) image = image_viewer.original_image - self.paint_tool = PaintTool(self.image_viewer.ax, image.shape, + self.paint_tool = PaintTool(image_viewer, image.shape, on_enter=self.on_enter) self.paint_tool.radius = self.radius self.paint_tool.label = self._label_widget.index = 1 diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 6e5b2c1f..46a0f1d2 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -59,7 +59,7 @@ class LineProfile(PlotPlugin): x = [w / 3, 2 * w / 3] y = [h / 2] * 2 - self.line_tool = ThickLineTool(self.image_viewer.ax, + self.line_tool = ThickLineTool(self.image_viewer, maxdist=self.maxdist, on_move=self.line_changed, on_change=self.line_changed) diff --git a/skimage/viewer/plugins/measure.py b/skimage/viewer/plugins/measure.py index eeaa13b0..7e29a796 100644 --- a/skimage/viewer/plugins/measure.py +++ b/skimage/viewer/plugins/measure.py @@ -32,7 +32,7 @@ class Measure(Plugin): image = image_viewer.original_image h, w = image.shape - self.line_tool = LineTool(self.image_viewer.ax, + self.line_tool = LineTool(self.image_viewer, maxdist=self.maxdist, on_move=self.line_changed) self.artists.append(self.line_tool) From 778f62de6a76e5eae0b0c78b66ab9567e326412a Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sun, 20 Jul 2014 19:15:40 -0500 Subject: [PATCH 0312/1122] Updates to get all tools tested and working --- skimage/viewer/canvastools/base.py | 5 ++++- skimage/viewer/canvastools/linetool.py | 15 +++++++-------- skimage/viewer/canvastools/painttool.py | 13 +++++++------ skimage/viewer/canvastools/recttool.py | 15 +++++++-------- skimage/viewer/viewers/core.py | 2 +- 5 files changed, 26 insertions(+), 24 deletions(-) diff --git a/skimage/viewer/canvastools/base.py b/skimage/viewer/canvastools/base.py index a1a06360..5412198b 100644 --- a/skimage/viewer/canvastools/base.py +++ b/skimage/viewer/canvastools/base.py @@ -46,11 +46,14 @@ class CanvasToolBase(object): """ return not self.active + def hit_test(self, event): + return False + def redraw(self): self.viewer.redraw() def set_visible(self, val): - for artist in self._artists: + for artist in self.artists: artist.set_visible(val) def on_key_press(self, event): diff --git a/skimage/viewer/canvastools/linetool.py b/skimage/viewer/canvastools/linetool.py index e1b48c05..0b6de5a4 100644 --- a/skimage/viewer/canvastools/linetool.py +++ b/skimage/viewer/canvastools/linetool.py @@ -37,7 +37,8 @@ class LineTool(CanvasToolBase): def __init__(self, viewer, on_move=None, on_release=None, on_enter=None, maxdist=10, line_props=None, **kwargs): - super(LineTool, self).__init__(ax, on_move=on_move, on_enter=on_enter, + super(LineTool, self).__init__(viewer, on_move=on_move, + on_enter=on_enter, on_release=on_release, **kwargs) props = dict(color='r', linewidth=1, alpha=0.4, solid_capstyle='butt') @@ -153,7 +154,7 @@ class ThickLineTool(LineTool): def __init__(self, viewer, on_move=None, on_enter=None, on_release=None, on_change=None, maxdist=10, line_props=None): - super(ThickLineTool, self).__init__(ax, + super(ThickLineTool, self).__init__(viewer, on_move=on_move, on_enter=on_enter, on_release=on_release, @@ -192,16 +193,14 @@ class ThickLineTool(LineTool): if __name__ == '__main__': # pragma: no cover - import matplotlib.pyplot as plt from skimage import data + from skimage.viewer import ImageViewer image = data.camera() - f, ax = plt.subplots() - ax.imshow(image, interpolation='nearest') + viewer = ImageViewer(image) h, w = image.shape - # line_tool = LineTool(ax) - line_tool = ThickLineTool(ax) + line_tool = ThickLineTool(viewer) line_tool.end_points = ([w/3, h/2], [2*w/3, h/2]) - plt.show() + viewer.show() diff --git a/skimage/viewer/canvastools/painttool.py b/skimage/viewer/canvastools/painttool.py index 8d3c9449..953f6ebe 100644 --- a/skimage/viewer/canvastools/painttool.py +++ b/skimage/viewer/canvastools/painttool.py @@ -44,7 +44,8 @@ class PaintTool(CanvasToolBase): def __init__(self, viewer, overlay_shape, radius=5, alpha=0.3, on_move=None, on_release=None, on_enter=None, rect_props=None): - super(PaintTool, self).__init__(ax, on_move=on_move, on_enter=on_enter, + super(PaintTool, self).__init__(viewer, on_move=on_move, + on_enter=on_enter, on_release=on_release) props = dict(edgecolor='r', facecolor='0.7', alpha=0.5, animated=True) @@ -65,7 +66,7 @@ class PaintTool(CanvasToolBase): # Note that the order is important: Redraw cursor *after* overlay self.artists = [self._overlay_plot, self._cursor] - view.add_tool(self) + viewer.add_tool(self) @property def label(self): @@ -201,10 +202,10 @@ class CenteredWindow(object): if __name__ == '__main__': # pragma: no cover np.testing.rundocs() from skimage import data + from skimage.viewer import ImageViewer image = data.camera() - f, ax = plt.subplots() - ax.imshow(image, interpolation='nearest') - paint_tool = PaintTool(ax, image.shape) - plt.show() + viewer = ImageViewer(image) + paint_tool = PaintTool(viewer, image.shape) + viewer.show() diff --git a/skimage/viewer/canvastools/recttool.py b/skimage/viewer/canvastools/recttool.py index d3724bf9..5d658533 100644 --- a/skimage/viewer/canvastools/recttool.py +++ b/skimage/viewer/canvastools/recttool.py @@ -41,7 +41,7 @@ class RectangleTool(CanvasToolBase, RectangleSelector): def __init__(self, viewer, on_move=None, on_release=None, on_enter=None, maxdist=10, rect_props=None): - CanvasToolBase.__init__(self, ax, on_move=on_move, + CanvasToolBase.__init__(self, viewer, on_move=on_move, on_enter=on_enter, on_release=on_release) props = dict(edgecolor=None, facecolor='r', alpha=0.15) @@ -67,11 +67,11 @@ class RectangleTool(CanvasToolBase, RectangleSelector): props = dict(mec=props['edgecolor']) self._corner_order = ['NW', 'NE', 'SE', 'SW'] xc, yc = self.corners - self._corner_handles = ToolHandles(ax, xc, yc, marker_props=props) + self._corner_handles = ToolHandles(self.ax, xc, yc, marker_props=props) self._edge_order = ['W', 'N', 'E', 'S'] xe, ye = self.edge_centers - self._edge_handles = ToolHandles(ax, xe, ye, marker='s', + self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s', marker_props=props) self.artists = [self._rect, @@ -202,13 +202,12 @@ class RectangleTool(CanvasToolBase, RectangleSelector): if __name__ == '__main__': # pragma: no cover - import matplotlib.pyplot as plt + from skimage.viewer import ImageViewer from skimage import data - f, ax = plt.subplots() - ax.imshow(data.camera(), interpolation='nearest') + viewer = ImageViewer(data.camera()) - rect_tool = RectangleTool(ax) - plt.show() + rect_tool = RectangleTool(viewer) + viewer.show() print("Final selection:") rect_tool.callback_on_enter(rect_tool.extents) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index afd3ff2b..6dc4a7a5 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -371,7 +371,7 @@ class ImageViewer(QtGui.QMainWindow): self.status_message('') def add_tool(self, tool): - if self.blit: + if self.useblit: self._blit_manager.artists.extend(tool.artists) self._tools.append(tool) self._event_manager.attach(tool) From 7864adbc0c36f81c614e3a7c11c8b9a553ee2583 Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sun, 20 Jul 2014 19:29:39 -0500 Subject: [PATCH 0313/1122] Make other suggested style changes --- skimage/viewer/viewers/core.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 6dc4a7a5..52e2259f 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -98,8 +98,7 @@ class EventManager(object): for tool in self.tools: if not tool.ignore(event) and tool.hit_test(event): self.active_tool = tool - tool.on_mouse_press(event) - return + break if self.active_tool and not self.active_tool.ignore(event): self.active_tool.on_mouse_press(event) return @@ -110,25 +109,23 @@ class EventManager(object): return def on_key_press(self, event): - tool = self.get_tool() - if not tool is None and not tool.ignore(event): + tool = self._get_tool(event) + if not tool is None: tool.on_key_press(event) - def get_tool(self): - if not self.tools: - return - if self.active_tool is None: - self.active_tool = self.tools[0] + def _get_tool(self, event): + if not self.tools or self.active_tool.ignore(event): + return None return self.active_tool def on_mouse_release(self, event): - tool = self.get_tool() - if not tool is None and not tool.ignore(event): + tool = self._get_tool(event) + if not tool is None: tool.on_mouse_release(event) def on_move(self, event): - tool = self.get_tool() - if not tool is None and not tool.ignore(event): + tool = self._get_tool(event) + if not tool is None: tool.on_move(event) From 17403921101f1fd55e263fd90172a23ef3b1dc18 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 1 Sep 2014 10:28:28 -0500 Subject: [PATCH 0314/1122] Add on_scroll behavior and allow line to change width. --- skimage/viewer/canvastools/linetool.py | 1 + skimage/viewer/viewers/core.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/skimage/viewer/canvastools/linetool.py b/skimage/viewer/canvastools/linetool.py index 0b6de5a4..6dbda97b 100644 --- a/skimage/viewer/canvastools/linetool.py +++ b/skimage/viewer/canvastools/linetool.py @@ -75,6 +75,7 @@ class LineTool(CanvasToolBase): self._line.set_data(np.transpose(pts)) self._handles.set_data(np.transpose(pts)) + self._line.set_linewidth(self.linewidth) self.set_visible(True) self.redraw() diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 52e2259f..f1e541a1 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -83,6 +83,7 @@ class EventManager(object): self.connect_event('key_press_event', self.on_key_press) self.connect_event('button_release_event', self.on_mouse_release) self.connect_event('motion_notify_event', self.on_move) + self.connect_event('scroll_event', self.on_scroll) self.tools = [] self.active_tool = None @@ -128,6 +129,11 @@ class EventManager(object): if not tool is None: tool.on_move(event) + def on_scroll(self, event): + tool = self._get_tool(event) + if not tool is None: + tool.on_scroll(event) + class ImageViewer(QtGui.QMainWindow): """Viewer for displaying images. From 1d9996680418420fef6a2dc83e5e9c8945a35794 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 1 Sep 2014 17:16:13 +0100 Subject: [PATCH 0315/1122] Remove leftovers of old logger --- skimage/__init__.py | 49 --------------------------------------------- 1 file changed, 49 deletions(-) diff --git a/skimage/__init__.py b/skimage/__init__.py index d1f98ff7..da7c983c 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -120,53 +120,4 @@ doctest.__doc__ = doctest.__doc__ doctest_verbose = _functools.partial(test, doctest=True, verbose=True) doctest_verbose.__doc__ = doctest.__doc__ - -class _Log(Warning): - pass - - -class _FakeLog(object): - def __init__(self, name): - """ - Parameters - ---------- - name : str - Name of the log. - repeat : bool - Whether to print repeating messages more than once (False by - default). - """ - self._name = name - - warnings.simplefilter("always", _Log) - - self._warnings = _warnings - - def _warn(self, msg, wtype): - self._warnings.warn('%s: %s' % (wtype, msg), _Log) - - def debug(self, msg): - self._warn(msg, 'DEBUG') - - def info(self, msg): - self._warn(msg, 'INFO') - - def warning(self, msg): - self._warn(msg, 'WARNING') - - warn = warning - - def error(self, msg): - self._warn(msg, 'ERROR') - - def critical(self, msg): - self._warn(msg, 'CRITICAL') - - def addHandler(*args): - pass - - def setLevel(*args): - pass - - from .util.dtype import * From 7ac6323210fbab64a417c1cf0f09ed8e5030f77e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 1 Sep 2014 13:30:07 -0500 Subject: [PATCH 0316/1122] Fix failing tests --- skimage/viewer/canvastools/base.py | 4 ++ skimage/viewer/tests/test_tools.py | 90 +++++++++++++--------------- skimage/viewer/tests/test_utils.py | 4 +- skimage/viewer/tests/test_widgets.py | 8 ++- skimage/viewer/viewers/core.py | 23 ++++++- 5 files changed, 73 insertions(+), 56 deletions(-) diff --git a/skimage/viewer/canvastools/base.py b/skimage/viewer/canvastools/base.py index 5412198b..197800c0 100644 --- a/skimage/viewer/canvastools/base.py +++ b/skimage/viewer/canvastools/base.py @@ -33,6 +33,7 @@ class CanvasToolBase(object): self.ax = viewer.ax self.artists = [] self.active = True + viewer.add_tool(self) self.callback_on_move = _pass if on_move is None else on_move self.callback_on_enter = _pass if on_enter is None else on_enter @@ -74,6 +75,9 @@ class CanvasToolBase(object): def on_scroll(self, event): pass + def remove(self): + self.viewer.remove_tool(self) + @property def geometry(self): """Geometry information that gets passed to callback functions.""" diff --git a/skimage/viewer/tests/test_tools.py b/skimage/viewer/tests/test_tools.py index 6663c465..8cc6bd0d 100644 --- a/skimage/viewer/tests/test_tools.py +++ b/skimage/viewer/tests/test_tools.py @@ -19,7 +19,7 @@ def get_end_points(image): return np.transpose([x, y]) -def create_mouse_event(ax, button=1, xdata=0, ydata=0, key=None): +def do_event(viewer, etype, button=1, xdata=0, ydata=0, key=None): """ *name* the event name @@ -56,6 +56,7 @@ def create_mouse_event(ax, button=1, xdata=0, ydata=0, key=None): *step* number of scroll steps (positive for 'up', negative for 'down') """ + ax = viewer.ax event = namedtuple('Event', ('name canvas guiEvent x y inaxes xdata ydata ' 'button key step')) @@ -68,7 +69,9 @@ def create_mouse_event(ax, button=1, xdata=0, ydata=0, key=None): event.step = 1 event.guiEvent = None event.name = 'Custom' - return event + + func = getattr(viewer._event_manager, 'on_%s' % etype) + func(event) @skipif(not viewer_available) @@ -76,24 +79,22 @@ def test_line_tool(): img = data.camera() viewer = ImageViewer(img) - tool = LineTool(viewer.ax, maxdist=10) + tool = LineTool(viewer, maxdist=10) tool.end_points = get_end_points(img) assert_equal(tool.end_points, np.array([[170, 256], [341, 256]])) # grab a handle and move it - grab = create_mouse_event(viewer.ax, xdata=170, ydata=256) - tool.on_mouse_press(grab) - move = create_mouse_event(viewer.ax, xdata=180, ydata=260) - tool.on_move(move) - tool.on_mouse_release(move) + do_event(viewer, 'mouse_press', xdata=170, ydata=256) + do_event(viewer, 'move', xdata=180, ydata=260) + do_event(viewer, 'mouse_release') + assert_equal(tool.geometry, np.array([[180, 260], [341, 256]])) # create a new line - new = create_mouse_event(viewer.ax, xdata=10, ydata=10) - tool.on_mouse_press(new) - move = create_mouse_event(viewer.ax, xdata=100, ydata=100) - tool.on_move(move) - tool.on_mouse_release(move) + do_event(viewer, 'mouse_press', xdata=10, ydata=10) + do_event(viewer, 'move', xdata=100, ydata=100) + do_event(viewer, 'mouse_release') + assert_equal(tool.geometry, np.array([[100, 100], [10, 10]])) @@ -102,23 +103,20 @@ def test_thick_line_tool(): img = data.camera() viewer = ImageViewer(img) - tool = ThickLineTool(viewer.ax, maxdist=10) + tool = ThickLineTool(viewer, maxdist=10) tool.end_points = get_end_points(img) - scroll_up = create_mouse_event(viewer.ax, button='up') - tool.on_scroll(scroll_up) + do_event(viewer, 'scroll', button='up') assert_equal(tool.linewidth, 2) - scroll_down = create_mouse_event(viewer.ax, button='down') - tool.on_scroll(scroll_down) + do_event(viewer, 'scroll', button='down') + assert_equal(tool.linewidth, 1) - key_up = create_mouse_event(viewer.ax, key='+') - tool.on_key_press(key_up) + do_event(viewer, 'key_press', key='+') assert_equal(tool.linewidth, 2) - key_down = create_mouse_event(viewer.ax, key='-') - tool.on_key_press(key_down) + do_event(viewer, 'key_press', key='-') assert_equal(tool.linewidth, 1) @@ -127,7 +125,7 @@ def test_rect_tool(): img = data.camera() viewer = ImageViewer(img) - tool = RectangleTool(viewer.ax, maxdist=10) + tool = RectangleTool(viewer, maxdist=10) tool.extents = (100, 150, 100, 150) assert_equal(tool.corners, @@ -138,19 +136,15 @@ def test_rect_tool(): assert_equal(tool.geometry, (100, 150, 100, 150)) # grab a corner and move it - grab = create_mouse_event(viewer.ax, xdata=100, ydata=100) - tool.press(grab) - move = create_mouse_event(viewer.ax, xdata=120, ydata=120) - tool.onmove(move) - tool.release(move) + do_event(viewer, 'mouse_press', xdata=100, ydata=100) + do_event(viewer, 'move', xdata=120, ydata=120) + do_event(viewer, 'mouse_release') assert_equal(tool.geometry, [120, 150, 120, 150]) # create a new line - new = create_mouse_event(viewer.ax, xdata=10, ydata=10) - tool.press(new) - move = create_mouse_event(viewer.ax, xdata=100, ydata=100) - tool.onmove(move) - tool.release(move) + do_event(viewer, 'mouse_press', xdata=10, ydata=10) + do_event(viewer, 'move', xdata=100, ydata=100) + do_event(viewer, 'mouse_release') assert_equal(tool.geometry, [10, 100, 10, 100]) @@ -159,7 +153,7 @@ def test_paint_tool(): img = data.moon() viewer = ImageViewer(img) - tool = PaintTool(viewer.ax, img.shape) + tool = PaintTool(viewer, img.shape) tool.radius = 10 assert_equal(tool.radius, 10) @@ -167,24 +161,21 @@ def test_paint_tool(): assert_equal(tool.label, 2) assert_equal(tool.shape, img.shape) - start = create_mouse_event(viewer.ax, xdata=100, ydata=100) - tool.on_mouse_press(start) - move = create_mouse_event(viewer.ax, xdata=110, ydata=110) - tool.on_move(move) - tool.on_mouse_release(move) + do_event(viewer, 'mouse_press', xdata=100, ydata=100) + do_event(viewer, 'move', xdata=110, ydata=110) + do_event(viewer, 'mouse_release') + assert_equal(tool.overlay[tool.overlay == 2].size, 761) tool.label = 5 - start = create_mouse_event(viewer.ax, xdata=20, ydata=20) - tool.on_mouse_press(start) - move = create_mouse_event(viewer.ax, xdata=40, ydata=40) - tool.on_move(move) - tool.on_mouse_release(move) + do_event(viewer, 'mouse_press', xdata=20, ydata=20) + do_event(viewer, 'move', xdata=40, ydata=40) + do_event(viewer, 'mouse_release') + assert_equal(tool.overlay[tool.overlay == 5].size, 881) assert_equal(tool.overlay[tool.overlay == 2].size, 761) - enter = create_mouse_event(viewer.ax, key='enter') - tool.on_mouse_press(enter) + do_event(viewer, 'key_press', key='enter') tool.overlay = tool.overlay * 0 assert_equal(tool.overlay.sum(), 0) @@ -195,15 +186,14 @@ def test_base_tool(): img = data.moon() viewer = ImageViewer(img) - tool = CanvasToolBase(viewer.ax) + tool = CanvasToolBase(viewer) tool.set_visible(False) tool.set_visible(True) - enter = create_mouse_event(viewer.ax, key='enter') - tool._on_key_press(enter) + do_event(viewer, 'key_press', key='enter') tool.redraw() tool.remove() - tool = CanvasToolBase(viewer.ax, useblit=False) + tool = CanvasToolBase(viewer, useblit=False) tool.redraw() diff --git a/skimage/viewer/tests/test_utils.py b/skimage/viewer/tests/test_utils.py index 3f9ec396..77f30299 100644 --- a/skimage/viewer/tests/test_utils.py +++ b/skimage/viewer/tests/test_utils.py @@ -28,7 +28,7 @@ def test_format_filename(): def test_open_file_dialog(): utils.init_qtapp() timer = QtCore.QTimer() - timer.singleShot(10, QtGui.QApplication.quit) + timer.singleShot(100, lambda: QtGui.QApplication.quit()) filename = dialogs.open_file_dialog() assert filename is None @@ -37,6 +37,6 @@ def test_open_file_dialog(): def test_save_file_dialog(): utils.init_qtapp() timer = QtCore.QTimer() - timer.singleShot(10, QtGui.QApplication.quit) + timer.singleShot(100, lambda: QtGui.QApplication.quit()) filename = dialogs.save_file_dialog() assert filename is None diff --git a/skimage/viewer/tests/test_widgets.py b/skimage/viewer/tests/test_widgets.py index ef3cb83c..ee4c91fb 100644 --- a/skimage/viewer/tests/test_widgets.py +++ b/skimage/viewer/tests/test_widgets.py @@ -77,11 +77,11 @@ def test_save_buttons(): viewer.plugins[0] += sv import tempfile - _, filename = tempfile.mkstemp(suffix='.png') - os.remove(filename) + fid, filename = tempfile.mkstemp(suffix='.png') + os.close(fid) timer = QtCore.QTimer() - timer.singleShot(100, lambda: QtGui.QApplication.quit()) + timer.singleShot(100, QtGui.QApplication.quit) sv.save_to_stack() sv.save_to_file(filename) @@ -92,6 +92,8 @@ def test_save_buttons(): img = io.pop() assert_almost_equal(img, viewer.image) + os.remove(filename) + @skipif(not viewer_available) def test_ok_buttons(): diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index f1e541a1..00902112 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -60,6 +60,14 @@ class BlitManager(object): self.background = None self.artists = [] + def add_artists(self, artists): + self.artists.extend(artists) + self.redraw() + + def remove_artists(self, artists): + for artist in artists: + self.artist.remove(artist) + def on_draw_event(self, event=None): self.background = self.canvas.copy_from_bbox(self.ax.bbox) self.draw_artists() @@ -95,6 +103,13 @@ class EventManager(object): self.tools.append(tool) self.active_tool = tool + def detach(self, tool): + self.tools.remove(tool) + if self.tools: + self.active_tool = self.tools[-1] + else: + self.active_tool = None + def on_mouse_press(self, event): for tool in self.tools: if not tool.ignore(event) and tool.hit_test(event): @@ -375,10 +390,16 @@ class ImageViewer(QtGui.QMainWindow): def add_tool(self, tool): if self.useblit: - self._blit_manager.artists.extend(tool.artists) + self._blit_manager.add_artists(tool.artists) self._tools.append(tool) self._event_manager.attach(tool) + def remove_tool(self, tool): + if self.useblit: + self._blit_manager.remove_artists(tool.artists) + self._tools.remove(tool) + self._event_manager.detach(tool) + def _format_coord(self, x, y): # callback function to format coordinate display in status bar x = int(x + 0.5) From 29763582f55733729e2f46d2fbc1c8c499286096 Mon Sep 17 00:00:00 2001 From: Geoffrey French Date: Mon, 1 Sep 2014 20:29:17 +0100 Subject: [PATCH 0317/1122] Fixed Python 3 error in plot_windowed_histogram caused by use of incorrect division operator. Also tweaked the similarity computation a little. --- doc/examples/plot_windowed_histogram.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/doc/examples/plot_windowed_histogram.py b/doc/examples/plot_windowed_histogram.py index 7b0bcb3a..cfa9345d 100644 --- a/doc/examples/plot_windowed_histogram.py +++ b/doc/examples/plot_windowed_histogram.py @@ -1,3 +1,4 @@ +from __future__ import division """ ======================== Sliding window histogram @@ -50,11 +51,8 @@ def windowed_histogram_similarity(image, selem, reference_hist, n_bins): # Generate a similarity measure. It needs to be low when distance is high. # and high when distance is low; taking the reciprocal will do this. - # Chi squared will always be >= 0. Add small value to prevent divide by 0. - # Square the denominator to push low values toward 0; this makes the - # high similarity regions stand out in the figure created below; this - # us just done for aesthetics. - similarity = 1 / (chi_sqr + 1.0e-6)**2 + # Chi squared will always be >= 0, add small value to prevent divide by 0. + similarity = 1 / (chi_sqr + 1.0e-4) return similarity @@ -65,7 +63,7 @@ img = img_as_ubyte(data.coins()) # Quantize to 16 levels of grayscale; this way the output image will have a # 16-dimensional feature vector per pixel -quantized_img = img/16 +quantized_img = img//16 # Select the coin from the 4th column, second row. # Co-ordinate ordering: [x1,y1,x2,y2] @@ -90,7 +88,7 @@ similarity = windowed_histogram_similarity(quantized_img, selem, coin_hist, # Now try a rotated image rotated_img = img_as_ubyte(transform.rotate(img, 45.0, resize=True)) # Quantize to 16 levels as before -quantized_rotated_image = rotated_img/16 +quantized_rotated_image = rotated_img//16 # Similarity on rotated image rotated_similarity = windowed_histogram_similarity(quantized_rotated_image, selem, coin_hist, From b9ac11da3f20dd30fe3c7bab30124df72c2fb152 Mon Sep 17 00:00:00 2001 From: Geoffrey French Date: Mon, 1 Sep 2014 22:39:41 +0100 Subject: [PATCH 0318/1122] Exchanged array output parameter for pointer and Py_ssize_t in kernel functions for slight speedup. --- skimage/filter/rank/bilateral_cy.pyx | 9 ++-- skimage/filter/rank/core_cy.pxd | 2 +- skimage/filter/rank/core_cy.pyx | 13 +++--- skimage/filter/rank/generic_cy.pyx | 61 ++++++++++++++++++--------- skimage/filter/rank/percentile_cy.pyx | 27 ++++++++---- 5 files changed, 72 insertions(+), 40 deletions(-) diff --git a/skimage/filter/rank/bilateral_cy.pyx b/skimage/filter/rank/bilateral_cy.pyx index 0e232465..20a97788 100644 --- a/skimage/filter/rank/bilateral_cy.pyx +++ b/skimage/filter/rank/bilateral_cy.pyx @@ -9,7 +9,8 @@ from libc.math cimport log from .core_cy cimport dtype_t, dtype_t_out, _core -cdef inline void _kernel_mean(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_mean(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -32,7 +33,8 @@ cdef inline void _kernel_mean(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_pop(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_pop(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -49,7 +51,8 @@ cdef inline void _kernel_pop(dtype_t_out[:] out, Py_ssize_t* histo, else: out[0] = 0 -cdef inline void _kernel_sum(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_sum(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, diff --git a/skimage/filter/rank/core_cy.pxd b/skimage/filter/rank/core_cy.pxd index 7d23f12a..9627c0d9 100644 --- a/skimage/filter/rank/core_cy.pxd +++ b/skimage/filter/rank/core_cy.pxd @@ -15,7 +15,7 @@ cdef dtype_t _max(dtype_t a, dtype_t b) cdef dtype_t _min(dtype_t a, dtype_t b) -cdef void _core(void kernel(dtype_t_out[:] out, Py_ssize_t*, double, +cdef void _core(void kernel(dtype_t_out*, Py_ssize_t, Py_ssize_t*, double, dtype_t, Py_ssize_t, Py_ssize_t, double, double, Py_ssize_t, Py_ssize_t), dtype_t[:, ::1] image, diff --git a/skimage/filter/rank/core_cy.pyx b/skimage/filter/rank/core_cy.pyx index 3c8c9e42..c1216c0d 100644 --- a/skimage/filter/rank/core_cy.pyx +++ b/skimage/filter/rank/core_cy.pyx @@ -42,7 +42,7 @@ cdef inline char is_in_mask(Py_ssize_t rows, Py_ssize_t cols, return 0 -cdef void _core(void kernel(dtype_t_out[:] out_feat, Py_ssize_t*, double, dtype_t, +cdef void _core(void kernel(dtype_t_out*, Py_ssize_t, Py_ssize_t*, double, dtype_t, Py_ssize_t, Py_ssize_t, double, double, Py_ssize_t, Py_ssize_t), dtype_t[:, ::1] image, @@ -61,6 +61,7 @@ cdef void _core(void kernel(dtype_t_out[:] out_feat, Py_ssize_t*, double, dtype_ cdef Py_ssize_t cols = image.shape[1] cdef Py_ssize_t srows = selem.shape[0] cdef Py_ssize_t scols = selem.shape[1] + cdef Py_ssize_t odepth = out.shape[2] cdef Py_ssize_t centre_r = (selem.shape[0] / 2) + shift_y cdef Py_ssize_t centre_c = (selem.shape[1] / 2) + shift_x @@ -151,7 +152,7 @@ cdef void _core(void kernel(dtype_t_out[:] out_feat, Py_ssize_t*, double, dtype_ r = 0 c = 0 - kernel(out[r, c, :], histo, pop, image[r, c], max_bin, mid_bin, + kernel(&out[r, c, 0], odepth, histo, pop, image[r, c], max_bin, mid_bin, p0, p1, s0, s1) # main loop @@ -172,7 +173,7 @@ cdef void _core(void kernel(dtype_t_out[:] out_feat, Py_ssize_t*, double, dtype_ if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - kernel(out[r, c, :], histo, pop, image[r, c], max_bin, mid_bin, + kernel(&out[r, c, 0], odepth, histo, pop, image[r, c], max_bin, mid_bin, p0, p1, s0, s1) r += 1 # pass to the next row @@ -192,7 +193,7 @@ cdef void _core(void kernel(dtype_t_out[:] out_feat, Py_ssize_t*, double, dtype_ if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - kernel(out[r, c, :], histo, pop, image[r, c], max_bin, mid_bin, + kernel(&out[r, c, 0], odepth, histo, pop, image[r, c], max_bin, mid_bin, p0, p1, s0, s1) # ---> east to west @@ -209,7 +210,7 @@ cdef void _core(void kernel(dtype_t_out[:] out_feat, Py_ssize_t*, double, dtype_ if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - kernel(out[r, c, :], histo, pop, image[r, c], max_bin, mid_bin, + kernel(&out[r, c, 0], odepth, histo, pop, image[r, c], max_bin, mid_bin, p0, p1, s0, s1) r += 1 # pass to the next row @@ -229,7 +230,7 @@ cdef void _core(void kernel(dtype_t_out[:] out_feat, Py_ssize_t*, double, dtype_ if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - kernel(out[r, c, :], histo, pop, image[r, c], + kernel(&out[r, c, 0], odepth, histo, pop, image[r, c], max_bin, mid_bin, p0, p1, s0, s1) # release memory allocated by malloc diff --git a/skimage/filter/rank/generic_cy.pyx b/skimage/filter/rank/generic_cy.pyx index bafd2432..bc54855f 100644 --- a/skimage/filter/rank/generic_cy.pyx +++ b/skimage/filter/rank/generic_cy.pyx @@ -9,7 +9,8 @@ from libc.math cimport log from .core_cy cimport dtype_t, dtype_t_out, _core -cdef inline void _kernel_autolevel(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_autolevel(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -35,7 +36,8 @@ cdef inline void _kernel_autolevel(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_bottomhat(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_bottomhat(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -52,7 +54,8 @@ cdef inline void _kernel_bottomhat(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_equalize(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_equalize(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -71,7 +74,8 @@ cdef inline void _kernel_equalize(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_gradient(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_gradient(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -93,7 +97,8 @@ cdef inline void _kernel_gradient(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_maximum(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_maximum(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -110,7 +115,8 @@ cdef inline void _kernel_maximum(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_mean(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_mean(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -127,7 +133,8 @@ cdef inline void _kernel_mean(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_subtract_mean(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_subtract_mean(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -144,7 +151,8 @@ cdef inline void _kernel_subtract_mean(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_median(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_median(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -164,7 +172,8 @@ cdef inline void _kernel_median(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_minimum(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_minimum(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -181,7 +190,8 @@ cdef inline void _kernel_minimum(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_modal(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_modal(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -199,7 +209,8 @@ cdef inline void _kernel_modal(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_enhance_contrast(dtype_t_out[:] out, +cdef inline void _kernel_enhance_contrast(dtype_t_out* out, + Py_ssize_t odepth, Py_ssize_t* histo, double pop, dtype_t g, @@ -227,7 +238,8 @@ cdef inline void _kernel_enhance_contrast(dtype_t_out[:] out, out[0] = 0 -cdef inline void _kernel_pop(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_pop(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -236,7 +248,8 @@ cdef inline void _kernel_pop(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = pop -cdef inline void _kernel_sum(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_sum(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -253,7 +266,8 @@ cdef inline void _kernel_sum(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_threshold(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_threshold(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -270,7 +284,8 @@ cdef inline void _kernel_threshold(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_tophat(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_tophat(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -287,7 +302,8 @@ cdef inline void _kernel_tophat(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_noise_filter(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_noise_filter(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -313,7 +329,8 @@ cdef inline void _kernel_noise_filter(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = min_i -cdef inline void _kernel_entropy(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_entropy(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -332,7 +349,8 @@ cdef inline void _kernel_entropy(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_otsu(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_otsu(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -371,7 +389,8 @@ cdef inline void _kernel_otsu(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = max_i -cdef inline void _kernel_win_hist(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_win_hist(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -381,10 +400,10 @@ cdef inline void _kernel_win_hist(dtype_t_out[:] out, Py_ssize_t* histo, cdef double scale if pop: scale = 1.0 / pop - for i in xrange(out.shape[0]): + for i in xrange(odepth): out[i] = (histo[i] * scale) else: - for i in xrange(out.shape[0]): + for i in xrange(odepth): out[i] = 0 diff --git a/skimage/filter/rank/percentile_cy.pyx b/skimage/filter/rank/percentile_cy.pyx index af33546d..6d54999f 100644 --- a/skimage/filter/rank/percentile_cy.pyx +++ b/skimage/filter/rank/percentile_cy.pyx @@ -7,7 +7,8 @@ cimport numpy as cnp from .core_cy cimport dtype_t, dtype_t_out, _core, _min, _max -cdef inline void _kernel_autolevel(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_autolevel(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -40,7 +41,8 @@ cdef inline void _kernel_autolevel(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_gradient(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_gradient(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -68,7 +70,8 @@ cdef inline void _kernel_gradient(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_mean(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_mean(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -93,7 +96,8 @@ cdef inline void _kernel_mean(dtype_t_out[:] out, Py_ssize_t* histo, else: out[0] = 0 -cdef inline void _kernel_sum(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_sum(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -118,7 +122,8 @@ cdef inline void _kernel_sum(dtype_t_out[:] out, Py_ssize_t* histo, else: out[0] = 0 -cdef inline void _kernel_subtract_mean(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_subtract_mean(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, @@ -144,7 +149,8 @@ cdef inline void _kernel_subtract_mean(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_enhance_contrast(dtype_t_out[:] out, +cdef inline void _kernel_enhance_contrast(dtype_t_out* out, + Py_ssize_t odepth, Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, @@ -180,7 +186,8 @@ cdef inline void _kernel_enhance_contrast(dtype_t_out[:] out, out[0] = 0 -cdef inline void _kernel_percentile(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_percentile(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -204,7 +211,8 @@ cdef inline void _kernel_percentile(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_pop(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_pop(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, @@ -224,7 +232,8 @@ cdef inline void _kernel_pop(dtype_t_out[:] out, Py_ssize_t* histo, out[0] = 0 -cdef inline void _kernel_threshold(dtype_t_out[:] out, Py_ssize_t* histo, +cdef inline void _kernel_threshold(dtype_t_out* out, Py_ssize_t odepth, + Py_ssize_t* histo, double pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, double p0, double p1, From 0936522064dd15daf856885b34fd6e513a55559e Mon Sep 17 00:00:00 2001 From: Geoffrey French Date: Mon, 1 Sep 2014 22:44:45 +0100 Subject: [PATCH 0319/1122] Improved plot_windowed_histogram doctoring, removed unnecessary imports and removed an internal testing line. --- doc/examples/plot_windowed_histogram.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/doc/examples/plot_windowed_histogram.py b/doc/examples/plot_windowed_histogram.py index cfa9345d..6d8f9ed0 100644 --- a/doc/examples/plot_windowed_histogram.py +++ b/doc/examples/plot_windowed_histogram.py @@ -4,13 +4,13 @@ from __future__ import division Sliding window histogram ======================== -This example extracts a single coin from the coins image and generates a -histogram of its greyscale values. +This example extracts a single coin from the `skimage.data.coins` image and +generates a histogram of its greyscale values. It then computes a sliding window histogram of the complete image using -rank.windowed_histogram. The local histogram for the region surrounding -each pixel in the image is compared to that of the single coin, with -a similarity measure being computed and displayed. +`skimage.filter.rank.windowed_histogram`. The local histogram for the region +surrounding each pixel in the image is compared to that of the single coin, +with a similarity measure being computed and displayed. To demonstrate the rotational invariance of the technique, the same test is performed on a version of the coins image rotated by 45 degrees. @@ -20,9 +20,7 @@ import matplotlib import matplotlib.pyplot as plt from skimage import data -from skimage.util.dtype import dtype_range from skimage.util import img_as_ubyte -from skimage import exposure from skimage.morphology import disk from skimage.filter import rank from skimage import transform @@ -57,9 +55,8 @@ def windowed_histogram_similarity(image, selem, reference_hist, n_bins): return similarity -# Load the coins image +# Load the `skimage.data.coins` image img = img_as_ubyte(data.coins()) -# img = img_as_ubyte(plt.imread('../../skimage/data/coins.png')) # Quantize to 16 levels of grayscale; this way the output image will have a # 16-dimensional feature vector per pixel From 9f5a6fddbfe21c9d3ba4bb4caa8310c44967e9b5 Mon Sep 17 00:00:00 2001 From: Geoffrey French Date: Mon, 1 Sep 2014 22:46:12 +0100 Subject: [PATCH 0320/1122] Fixed comment in plot_windowed_histogram. --- doc/examples/plot_windowed_histogram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/examples/plot_windowed_histogram.py b/doc/examples/plot_windowed_histogram.py index 6d8f9ed0..73cf82a7 100644 --- a/doc/examples/plot_windowed_histogram.py +++ b/doc/examples/plot_windowed_histogram.py @@ -37,7 +37,7 @@ def windowed_histogram_similarity(image, selem, reference_hist, n_bins): # arithmetic operations with the windowed histograms fro the image reference_hist = reference_hist.reshape((1,1) + reference_hist.shape) - # Compute Chi squared distance metric: sum((X-Y)**2 / (X+Y); + # Compute Chi squared distance metric: sum((X-Y)^2 / (X+Y)); # a measure of distance between histograms X = px_histograms Y = reference_hist From a90096555ba7c252bf5dabea8d5e98f7f1d60fc1 Mon Sep 17 00:00:00 2001 From: Geoffrey French Date: Mon, 1 Sep 2014 22:59:59 +0100 Subject: [PATCH 0321/1122] Docstring and comment improvements and fixes in plot_windowed_histogram. Readability improvement to skimage/io/__init__.py --- doc/examples/plot_windowed_histogram.py | 10 ++++++++-- skimage/io/__init__.py | 5 ++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/doc/examples/plot_windowed_histogram.py b/doc/examples/plot_windowed_histogram.py index 73cf82a7..8e2c4384 100644 --- a/doc/examples/plot_windowed_histogram.py +++ b/doc/examples/plot_windowed_histogram.py @@ -12,6 +12,12 @@ It then computes a sliding window histogram of the complete image using surrounding each pixel in the image is compared to that of the single coin, with a similarity measure being computed and displayed. +The histogram of the single coin is computed using `numpy.histogram` on a +box shaped region surrounding the coin, while the sliding window histograms +are computed using a disc shaped structural element of a slightly different +size. This is done in aid of demonstrating that the technique still finds +similarity inspite of these differences. + To demonstrate the rotational invariance of the technique, the same test is performed on a version of the coins image rotated by 45 degrees. """ @@ -34,7 +40,7 @@ def windowed_histogram_similarity(image, selem, reference_hist, n_bins): px_histograms = rank.windowed_histogram(image, selem, n_bins=n_bins) # Reshape coin histogram to (1,1,N) for broadcast when we want to use it in - # arithmetic operations with the windowed histograms fro the image + # arithmetic operations with the windowed histograms from the image reference_hist = reference_hist.reshape((1,1) + reference_hist.shape) # Compute Chi squared distance metric: sum((X-Y)^2 / (X+Y)); @@ -47,7 +53,7 @@ def windowed_histogram_similarity(image, selem, reference_hist, n_bins): frac[denom==0] = 0 chi_sqr = np.sum(frac, axis=2) * 0.5 - # Generate a similarity measure. It needs to be low when distance is high. + # Generate a similarity measure. It needs to be low when distance is high # and high when distance is low; taking the reciprocal will do this. # Chi squared will always be >= 0, add small value to prevent divide by 0. similarity = 1 / (chi_sqr + 1.0e-4) diff --git a/skimage/io/__init__.py b/skimage/io/__init__.py index 8f3b9bca..85461216 100644 --- a/skimage/io/__init__.py +++ b/skimage/io/__init__.py @@ -40,7 +40,10 @@ def _update_doc(doc): info_table = [(p, plugin_info(p).get('description', 'no description')) for p in available_plugins if not p == 'test'] - name_length = max([len(n) for (n, _) in info_table]) if len(info_table) > 0 else 0 + if len(info_table) > 0: + name_length = max([len(n) for (n, _) in info_table]) + else: + name_length = 0 description_length = WRAP_LEN - 1 - name_length column_lengths = [name_length, description_length] From bf02a92ee399208874171fa251ee92e55cc61515 Mon Sep 17 00:00:00 2001 From: Geoffrey French Date: Mon, 1 Sep 2014 23:04:24 +0100 Subject: [PATCH 0322/1122] Small fix to windowed_histogram doctoring. --- skimage/filter/rank/generic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index 2af2428e..cec1624d 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -887,8 +887,8 @@ def windowed_histogram(image, selem, out=None, mask=None, shift_x=False, shift_y to the structuring element sizes (center must be inside the given structuring element). n_bins : int or None - The number of histogram bins. Will default to image.max() + 1 if None - is passed. + The number of histogram bins. Will default to `image.max() + 1` + if None is passed. Returns ------- From e6bda5accd0d9133e37eb1d9ed456c5191d8db19 Mon Sep 17 00:00:00 2001 From: Geoffrey French Date: Mon, 1 Sep 2014 23:41:03 +0100 Subject: [PATCH 0323/1122] Fix to skimage.filter.rank.windowed_histogram docstring. Better explanation of technique in plot_windowed_histogram example, along with (hopefully correct) citations. Relevant additions to release_dev.txt and CONTRIBUTORS.txt. --- CONTRIBUTORS.txt | 3 +++ doc/examples/plot_windowed_histogram.py | 30 +++++++++++++++++++------ doc/release/release_dev.txt | 1 + skimage/filter/rank/generic.py | 8 +++---- 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 30f432f4..b0a82519 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -185,3 +185,6 @@ - Adam Feuer PIL Image import and export improvements + +- Geoffrey French + skimage.filters.rank.windowed_histogram and plot_windowed_histogram example. \ No newline at end of file diff --git a/doc/examples/plot_windowed_histogram.py b/doc/examples/plot_windowed_histogram.py index 8e2c4384..2cc77139 100644 --- a/doc/examples/plot_windowed_histogram.py +++ b/doc/examples/plot_windowed_histogram.py @@ -4,22 +4,38 @@ from __future__ import division Sliding window histogram ======================== -This example extracts a single coin from the `skimage.data.coins` image and -generates a histogram of its greyscale values. +Histogram matching can be used for object detection in images [1]_. +This example extracts a single coin from the `skimage.data.coins` image +and uses histogram matching to attempt to locate it within the original +image. -It then computes a sliding window histogram of the complete image using -`skimage.filter.rank.windowed_histogram`. The local histogram for the region -surrounding each pixel in the image is compared to that of the single coin, -with a similarity measure being computed and displayed. +First, a box-shaped region of the image containing the target coin is +extracted and a histogram of its greyscale values is computed. + +Next, for each pixel in the test image, a histogram of the greyscale values +in a region of the image surrounding the pixel is computed. +`skimage.filter.rank.windowed_histogram` is used for this task, as it +employs an efficient sliding window based algorithm that is able to compute +these histograms quickly [2]_. +The local histogram for the region surrounding each pixel in the image is +compared to that of the single coin, with a similarity measure being +computed and displayed. The histogram of the single coin is computed using `numpy.histogram` on a box shaped region surrounding the coin, while the sliding window histograms are computed using a disc shaped structural element of a slightly different size. This is done in aid of demonstrating that the technique still finds -similarity inspite of these differences. +similarity in spite of these differences. To demonstrate the rotational invariance of the technique, the same test is performed on a version of the coins image rotated by 45 degrees. + +References +---------- +.. [1] Porikli, F. "Integral Histogram: A Fast Way to Extract Histograms + in Cartesian Spaces" CVPR, 2005. Vol. 1. IEEE, 2005 +.. [2] S.Perreault and P.Hebert. Median filtering in constant time. + Trans. Image Processing, 16(9):2389-2394, 2007. """ import numpy as np import matplotlib diff --git a/doc/release/release_dev.txt b/doc/release/release_dev.txt index cbeded27..77c8d102 100644 --- a/doc/release/release_dev.txt +++ b/doc/release/release_dev.txt @@ -20,6 +20,7 @@ Region Adjacency Graphs - Similarity RAGs (#1080) - Normalized Cut on RAGs (#1080) - RAG Drawing (#1087) +Sliding Windowed Histogram (#1127) Improvements ------------ diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index cec1624d..018d7c0b 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -895,10 +895,10 @@ def windowed_histogram(image, selem, out=None, mask=None, shift_x=False, shift_y out : 3-D array with float dtype of dimensions (H,W,N), where (H,W) are the dimensions of the input image and N is n_bins or image.max()+1 if no value is provided as a parameter. Effectively, each pixel - is an N-dimensional feature vector that is the histogram. - The sum of the elements in the feature vector will be 1, unless - no pixels in the window were covered by both selem and mask, in which - case all elements will be 0. + is a N-D feature vector that is the histogram. The sum of the + elements in the feature vector will be 1, unless no pixels in the + window were covered by both selem and mask, in which case all + elements will be 0. Examples -------- From 118ce124bf11c58077f2e384664faca0f8072bd3 Mon Sep 17 00:00:00 2001 From: capitanbatata Date: Tue, 2 Sep 2014 08:01:22 +0200 Subject: [PATCH 0324/1122] Undone change in skimage/__init__.py. --- skimage/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/skimage/__init__.py b/skimage/__init__.py index aacde5d6..59c80ed2 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -65,11 +65,11 @@ from skimage._shared.utils import deprecated as _deprecated pkg_dir = _osp.abspath(_osp.dirname(__file__)) data_dir = _osp.join(pkg_dir, 'data') -# try: -# from .version import version as __version__ -# except ImportError: -# __version__ = "unbuilt-dev" -# del version +try: + from .version import version as __version__ +except ImportError: + __version__ = "unbuilt-dev" +del version try: From ceae401d331a8942a43dc3b44dd0402a504aa3bf Mon Sep 17 00:00:00 2001 From: capitanbatata Date: Tue, 2 Sep 2014 13:53:11 +0200 Subject: [PATCH 0325/1122] The format strings were updated to include numbers. --- skimage/color/tests/test_colorconv.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index faf9b9db..0c0fe507 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -236,14 +236,14 @@ class TestColorconv(TestCase): ## Thest the conversion with the rest of the illuminants. for I in ["d50", "d55", "d65", "d75"]: for obs in [2, 10]: - print("testing illuminant={}, observer={}".format(I, obs)) - fname = "lab_array_{}_{}.npy".format(I, obs) + print("testing illuminant={0}, observer={1}".format(I, obs)) + fname = "lab_array_{0}_{1}.npy".format(I, obs) lab_array_I_obs = np.load(os.path.join('data', fname)) assert_array_almost_equal(lab_array_I_obs, xyz2lab(self.xyz_array, I, obs), decimal=2) for I in ["a", "e"]: - print("testing illuminant={}, observer=2".format(I)) - fname = "lab_array_{}_2.npy".format(I) + print("testing illuminant={0}, observer=2".format(I)) + fname = "lab_array_{0}_2.npy".format(I) lab_array_I_obs = np.load(os.path.join('data', fname)) assert_array_almost_equal(lab_array_I_obs, xyz2lab(self.xyz_array, I, 2), decimal=2) @@ -255,12 +255,12 @@ class TestColorconv(TestCase): ## Thest the conversion with the rest of the illuminants. for I in ["d50", "d55", "d65", "d75"]: for obs in [2, 10]: - fname = "lab_array_{}_{}.npy".format(I, obs) + fname = "lab_array_{0}_{1}.npy".format(I, obs) lab_array_I_obs = np.load(os.path.join('data', fname)) assert_array_almost_equal(lab2xyz(lab_array_I_obs, I, obs), self.xyz_array, decimal=3) for I in ["a", "e"]: - fname = "lab_array_{}_2.npy".format(I, obs) + fname = "lab_array_{0}_2.npy".format(I, obs) lab_array_I_obs = np.load(os.path.join('data', fname)) assert_array_almost_equal(lab2xyz(lab_array_I_obs, I, 2), self.xyz_array, decimal=3) @@ -299,14 +299,14 @@ class TestColorconv(TestCase): ## Thest the conversion with the rest of the illuminants. for I in ["d50", "d55", "d65", "d75"]: for obs in [2, 10]: - print("testing illuminant={}, observer={}".format(I, obs)) - fname = "luv_array_{}_{}.npy".format(I, obs) + print("testing illuminant={0}, observer={1}".format(I, obs)) + fname = "luv_array_{0}_{1}.npy".format(I, obs) luv_array_I_obs = np.load(os.path.join('data', fname)) assert_array_almost_equal(luv_array_I_obs, xyz2luv(self.xyz_array, I, obs), decimal=2) for I in ["a", "e"]: - print("testing illuminant={}, observer=2".format(I)) - fname = "luv_array_{}_2.npy".format(I) + print("testing illuminant={0}, observer=2".format(I)) + fname = "luv_array_{0}_2.npy".format(I) luv_array_I_obs = np.load(os.path.join('data', fname)) assert_array_almost_equal(luv_array_I_obs, xyz2luv(self.xyz_array, I, 2), decimal=2) @@ -319,12 +319,12 @@ class TestColorconv(TestCase): ## Thest the conversion with the rest of the illuminants. for I in ["d50", "d55", "d65", "d75"]: for obs in [2, 10]: - fname = "luv_array_{}_{}.npy".format(I, obs) + fname = "luv_array_{0}_{1}.npy".format(I, obs) luv_array_I_obs = np.load(os.path.join('data', fname)) assert_array_almost_equal(luv2xyz(luv_array_I_obs, I, obs), self.xyz_array, decimal=3) for I in ["a", "e"]: - fname = "luv_array_{}_2.npy".format(I, obs) + fname = "luv_array_{0}_2.npy".format(I, obs) luv_array_I_obs = np.load(os.path.join('data', fname)) assert_array_almost_equal(luv2xyz(luv_array_I_obs, I, 2), self.xyz_array, decimal=3) From ffe7156f317bd3bdfa2d3395e21d8fc1a52abd40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 2 Sep 2014 09:44:33 -0400 Subject: [PATCH 0326/1122] Stephan's comments --- doc/source/user_guide/parallelization.txt | 28 +++++++++-------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/doc/source/user_guide/parallelization.txt b/doc/source/user_guide/parallelization.txt index 20517de5..f0323c7d 100755 --- a/doc/source/user_guide/parallelization.txt +++ b/doc/source/user_guide/parallelization.txt @@ -1,13 +1,13 @@ -========================= -How to parallelize loops? -========================= +======================== +How to parallelize loops +======================== In image processing, we frequently apply the same algorithm -on a large batch of images. Let us define an example. +on a large batch of images. Here is an example: .. code-block:: python - from skimage import data, color + from skimage import data, color, util from skimage.restoration import denoise_tv_chambolle from skimage.feature import hog @@ -25,24 +25,18 @@ on a large batch of images. Let us define an example. # Prepare images hubble = data.hubble_deep_field() width = 10 - pics = [hubble[:,slice:slice+width] for slice in range(0, 1000, width)] + pics = util.view_as_windows(hubble, (width, hubble.shape[1], hubble.shape[2]), step=width) To call the function ``task`` on each element of the list ``pics``, it is -usual to write a for loop. To measure the execution time of this loop, a function -is defined and called with ``timeit``. +usual to write a for loop. To measure the execution time of this loop, you can +use ipython and measure the execution time with ``%timeit``. .. code-block:: python def classic_loop(): for image in pics: - task(image) + task(image[0][0]) - import timeit - print("classic_loop():", timeit.timeit("classic_loop()", setup="from __main__ import (classic_loop, task, pics)", number=1)) - -Alternatively, you can use ipython and measure the execution time with ``%timeit``. - -.. code-block:: python %timeit classic_loop() @@ -51,7 +45,7 @@ Another equivalent way to code this loop is to use a comprehension list which ha .. code-block:: python def comprehension_loop(): - [task(image) for image in pics] + [task(image[0][0]) for image in pics] %timeit comprehension_loop() @@ -62,6 +56,6 @@ The number of jobs can be specified. from joblib import Parallel, delayed def joblib_loop(): - Parallel(n_jobs=4)(delayed(task)(i) for i in pics) + Parallel(n_jobs=4)(delayed(task)(i[0][0]) for i in pics) %timeit joblib_loop() From 5342299572dc26b7bff56ec0999e1158860b5e2a Mon Sep 17 00:00:00 2001 From: Geoffrey French Date: Tue, 2 Sep 2014 19:16:41 +0100 Subject: [PATCH 0327/1122] PEP8 compliance and doc formatting fixes. --- doc/examples/plot_windowed_histogram.py | 14 ++--- skimage/filter/rank/bilateral.py | 3 +- skimage/filter/rank/bilateral_cy.pyx | 2 + skimage/filter/rank/core_cy.pyx | 16 ++--- skimage/filter/rank/generic.py | 84 ++++++++++++++++--------- skimage/filter/rank/generic_cy.pyx | 2 +- skimage/filter/rank/percentile_cy.pyx | 2 + 7 files changed, 75 insertions(+), 48 deletions(-) diff --git a/doc/examples/plot_windowed_histogram.py b/doc/examples/plot_windowed_histogram.py index 2cc77139..1c57721b 100644 --- a/doc/examples/plot_windowed_histogram.py +++ b/doc/examples/plot_windowed_histogram.py @@ -57,7 +57,7 @@ def windowed_histogram_similarity(image, selem, reference_hist, n_bins): # Reshape coin histogram to (1,1,N) for broadcast when we want to use it in # arithmetic operations with the windowed histograms from the image - reference_hist = reference_hist.reshape((1,1) + reference_hist.shape) + reference_hist = reference_hist.reshape((1, 1) + reference_hist.shape) # Compute Chi squared distance metric: sum((X-Y)^2 / (X+Y)); # a measure of distance between histograms @@ -66,7 +66,7 @@ def windowed_histogram_similarity(image, selem, reference_hist, n_bins): num = (X-Y)*(X-Y) denom = X+Y frac = num / denom - frac[denom==0] = 0 + frac[denom == 0] = 0 chi_sqr = np.sum(frac, axis=2) * 0.5 # Generate a similarity measure. It needs to be low when distance is high @@ -80,17 +80,18 @@ def windowed_histogram_similarity(image, selem, reference_hist, n_bins): # Load the `skimage.data.coins` image img = img_as_ubyte(data.coins()) -# Quantize to 16 levels of grayscale; this way the output image will have a +# Quantize to 16 levels of greyscale; this way the output image will have a # 16-dimensional feature vector per pixel quantized_img = img//16 # Select the coin from the 4th column, second row. # Co-ordinate ordering: [x1,y1,x2,y2] -coin_coords = [184,100,228,148] # 44 x 44 region -coin = quantized_img[coin_coords[1]:coin_coords[3], coin_coords[0]:coin_coords[2]] +coin_coords = [184, 100, 228, 148] # 44 x 44 region +coin = quantized_img[coin_coords[1]:coin_coords[3], + coin_coords[0]:coin_coords[2]] # Compute coin histogram and normalize -coin_hist, _ = np.histogram(coin.flatten(), bins=16, range=(0,16)) +coin_hist, _ = np.histogram(coin.flatten(), bins=16, range=(0, 16)) coin_hist = coin_hist.astype(float) / np.sum(coin_hist) @@ -114,7 +115,6 @@ rotated_similarity = windowed_histogram_similarity(quantized_rotated_image, coin_hist.shape[0]) - # Plot it all fig, axes = plt.subplots(nrows=5, figsize=(6, 18)) ax0, ax1, ax2, ax3, ax4 = axes diff --git a/skimage/filter/rank/bilateral.py b/skimage/filter/rank/bilateral.py index 73147c7f..aeb318d1 100644 --- a/skimage/filter/rank/bilateral.py +++ b/skimage/filter/rank/bilateral.py @@ -158,8 +158,9 @@ def pop_bilateral(image, selem, out=None, mask=None, shift_x=False, return _apply(bilateral_cy._pop, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, s0=s0, s1=s1) + def sum_bilateral(image, selem, out=None, mask=None, shift_x=False, - shift_y=False, s0=10, s1=10): + shift_y=False, s0=10, s1=10): """Apply a flat kernel bilateral filter. This is an edge-preserving and noise reducing denoising filter. It averages diff --git a/skimage/filter/rank/bilateral_cy.pyx b/skimage/filter/rank/bilateral_cy.pyx index 20a97788..b4e22d3f 100644 --- a/skimage/filter/rank/bilateral_cy.pyx +++ b/skimage/filter/rank/bilateral_cy.pyx @@ -51,6 +51,7 @@ cdef inline void _kernel_pop(dtype_t_out* out, Py_ssize_t odepth, else: out[0] = 0 + cdef inline void _kernel_sum(dtype_t_out* out, Py_ssize_t odepth, Py_ssize_t* histo, double pop, dtype_t g, @@ -96,6 +97,7 @@ def _pop(dtype_t[:, ::1] image, _core(_kernel_pop[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, s0, s1, max_bin) + def _sum(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, diff --git a/skimage/filter/rank/core_cy.pyx b/skimage/filter/rank/core_cy.pyx index c1216c0d..4bd21df0 100644 --- a/skimage/filter/rank/core_cy.pyx +++ b/skimage/filter/rank/core_cy.pyx @@ -42,8 +42,8 @@ cdef inline char is_in_mask(Py_ssize_t rows, Py_ssize_t cols, return 0 -cdef void _core(void kernel(dtype_t_out*, Py_ssize_t, Py_ssize_t*, double, dtype_t, - Py_ssize_t, Py_ssize_t, double, +cdef void _core(void kernel(dtype_t_out*, Py_ssize_t, Py_ssize_t*, double, + dtype_t, Py_ssize_t, Py_ssize_t, double, double, Py_ssize_t, Py_ssize_t), dtype_t[:, ::1] image, char[:, ::1] selem, @@ -173,8 +173,8 @@ cdef void _core(void kernel(dtype_t_out*, Py_ssize_t, Py_ssize_t*, double, dtype if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - kernel(&out[r, c, 0], odepth, histo, pop, image[r, c], max_bin, mid_bin, - p0, p1, s0, s1) + kernel(&out[r, c, 0], odepth, histo, pop, image[r, c], max_bin, + mid_bin, p0, p1, s0, s1) r += 1 # pass to the next row if r >= rows: @@ -193,8 +193,8 @@ cdef void _core(void kernel(dtype_t_out*, Py_ssize_t, Py_ssize_t*, double, dtype if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - kernel(&out[r, c, 0], odepth, histo, pop, image[r, c], max_bin, mid_bin, - p0, p1, s0, s1) + kernel(&out[r, c, 0], odepth, histo, pop, image[r, c], max_bin, + mid_bin, p0, p1, s0, s1) # ---> east to west for c in range(cols - 2, -1, -1): @@ -210,8 +210,8 @@ cdef void _core(void kernel(dtype_t_out*, Py_ssize_t, Py_ssize_t*, double, dtype if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - kernel(&out[r, c, 0], odepth, histo, pop, image[r, c], max_bin, mid_bin, - p0, p1, s0, s1) + kernel(&out[r, c, 0], odepth, histo, pop, image[r, c], max_bin, + mid_bin, p0, p1, s0, s1) r += 1 # pass to the next row if r >= rows: diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index 018d7c0b..f2e75051 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -68,7 +68,8 @@ def _handle_input(image, selem, out, mask, out_dtype=None, pixel_size=1): return image, selem, out, mask, max_bin -def _apply_scalar_per_pixel(func, image, selem, out, mask, shift_x, shift_y, out_dtype=None): +def _apply_scalar_per_pixel(func, image, selem, out, mask, shift_x, shift_y, + out_dtype=None): image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, out_dtype) @@ -79,10 +80,12 @@ def _apply_scalar_per_pixel(func, image, selem, out, mask, shift_x, shift_y, out return out.reshape(out.shape[:2]) -def _apply_vector_per_pixel(func, image, selem, out, mask, shift_x, shift_y, out_dtype=None, pixel_size=1): +def _apply_vector_per_pixel(func, image, selem, out, mask, shift_x, shift_y, + out_dtype=None, pixel_size=1): image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, - out_dtype, pixel_size=pixel_size) + out_dtype, + pixel_size=pixel_size) func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, out=out, max_bin=max_bin) @@ -128,7 +131,8 @@ def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ return _apply_scalar_per_pixel(generic_cy._autolevel, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def bottomhat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -169,7 +173,8 @@ def bottomhat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ return _apply_scalar_per_pixel(generic_cy._bottomhat, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -207,7 +212,8 @@ def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ return _apply_scalar_per_pixel(generic_cy._equalize, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -245,7 +251,8 @@ def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ return _apply_scalar_per_pixel(generic_cy._gradient, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -292,7 +299,8 @@ def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ return _apply_scalar_per_pixel(generic_cy._maximum, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -330,7 +338,8 @@ def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ return _apply_scalar_per_pixel(generic_cy._mean, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def subtract_mean(image, selem, out=None, mask=None, shift_x=False, @@ -369,7 +378,8 @@ def subtract_mean(image, selem, out=None, mask=None, shift_x=False, """ return _apply_scalar_per_pixel(generic_cy._subtract_mean, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -407,7 +417,8 @@ def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ return _apply_scalar_per_pixel(generic_cy._median, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -454,7 +465,8 @@ def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ return _apply_scalar_per_pixel(generic_cy._minimum, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -494,7 +506,8 @@ def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ return _apply_scalar_per_pixel(generic_cy._modal, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def enhance_contrast(image, selem, out=None, mask=None, shift_x=False, @@ -537,7 +550,8 @@ def enhance_contrast(image, selem, out=None, mask=None, shift_x=False, """ return _apply_scalar_per_pixel(generic_cy._enhance_contrast, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -586,7 +600,8 @@ def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ return _apply_scalar_per_pixel(generic_cy._pop, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) + mask=mask, shift_x=shift_x, + shift_y=shift_y) def sum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -635,7 +650,8 @@ def sum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ return _apply_scalar_per_pixel(generic_cy._sum, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) + mask=mask, shift_x=shift_x, + shift_y=shift_y) def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -684,7 +700,8 @@ def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ return _apply_scalar_per_pixel(generic_cy._threshold, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -725,7 +742,8 @@ def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ return _apply_scalar_per_pixel(generic_cy._tophat, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def noise_filter(image, selem, out=None, mask=None, shift_x=False, @@ -775,8 +793,9 @@ def noise_filter(image, selem, out=None, mask=None, shift_x=False, selem_cpy = selem.copy() selem_cpy[centre_r, centre_c] = 0 - return _apply_scalar_per_pixel(generic_cy._noise_filter, image, selem_cpy, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) + return _apply_scalar_per_pixel(generic_cy._noise_filter, image, selem_cpy, + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y) def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -821,8 +840,9 @@ def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ return _apply_scalar_per_pixel(generic_cy._entropy, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, - out_dtype=np.double) + out=out, mask=mask, + shift_x=shift_x, shift_y=shift_y, + out_dtype=np.double) def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -865,10 +885,12 @@ def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ return _apply_scalar_per_pixel(generic_cy._otsu, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) + mask=mask, shift_x=shift_x, + shift_y=shift_y) -def windowed_histogram(image, selem, out=None, mask=None, shift_x=False, shift_y=False, n_bins=None): +def windowed_histogram(image, selem, out=None, mask=None, + shift_x=False, shift_y=False, n_bins=None): """Normalized sliding window histogram Parameters @@ -887,18 +909,18 @@ def windowed_histogram(image, selem, out=None, mask=None, shift_x=False, shift_y to the structuring element sizes (center must be inside the given structuring element). n_bins : int or None - The number of histogram bins. Will default to `image.max() + 1` + The number of histogram bins. Will default to ``image.max() + 1`` if None is passed. Returns ------- out : 3-D array with float dtype of dimensions (H,W,N), where (H,W) are - the dimensions of the input image and N is n_bins or image.max()+1 - if no value is provided as a parameter. Effectively, each pixel - is a N-D feature vector that is the histogram. The sum of the - elements in the feature vector will be 1, unless no pixels in the - window were covered by both selem and mask, in which case all - elements will be 0. + the dimensions of the input image and N is n_bins or + ``image.max() + 1`` if no value is provided as a parameter. + Effectively, each pixel is a N-D feature vector that is the histogram. + The sum of the elements in the feature vector will be 1, unless no + pixels in the window were covered by both selem and mask, in which + case all elements will be 0. Examples -------- diff --git a/skimage/filter/rank/generic_cy.pyx b/skimage/filter/rank/generic_cy.pyx index bc54855f..98c8cd8a 100644 --- a/skimage/filter/rank/generic_cy.pyx +++ b/skimage/filter/rank/generic_cy.pyx @@ -407,7 +407,6 @@ cdef inline void _kernel_win_hist(dtype_t_out* out, Py_ssize_t odepth, out[i] = 0 - def _autolevel(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, @@ -527,6 +526,7 @@ def _pop(dtype_t[:, ::1] image, _core(_kernel_pop[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) + def _sum(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, diff --git a/skimage/filter/rank/percentile_cy.pyx b/skimage/filter/rank/percentile_cy.pyx index 6d54999f..5ff584d3 100644 --- a/skimage/filter/rank/percentile_cy.pyx +++ b/skimage/filter/rank/percentile_cy.pyx @@ -285,6 +285,7 @@ def _mean(dtype_t[:, ::1] image, _core(_kernel_mean[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, p0, p1, 0, 0, max_bin) + def _sum(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, @@ -295,6 +296,7 @@ def _sum(dtype_t[:, ::1] image, _core(_kernel_sum[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, p0, p1, 0, 0, max_bin) + def _subtract_mean(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, From c518f78ff72e6b4a65e3493d42db610ad615c3b9 Mon Sep 17 00:00:00 2001 From: Geoffrey French Date: Tue, 2 Sep 2014 20:17:43 +0100 Subject: [PATCH 0328/1122] Fixed mistake in generic.py --- skimage/filter/rank/generic.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index f2e75051..a474d050 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -338,8 +338,7 @@ def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ return _apply_scalar_per_pixel(generic_cy._mean, image, selem, out=out, - out=out, mask=mask, - shift_x=shift_x, shift_y=shift_y) + mask=mask, shift_x=shift_x, shift_y=shift_y) def subtract_mean(image, selem, out=None, mask=None, shift_x=False, From 9ee87498bae157a9c5103d66cd0fee09e7eae1a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 2 Sep 2014 19:18:43 -0400 Subject: [PATCH 0329/1122] PEP8 fixes and rearranged images in plot --- doc/examples/plot_windowed_histogram.py | 91 +++++++++++-------------- 1 file changed, 40 insertions(+), 51 deletions(-) diff --git a/doc/examples/plot_windowed_histogram.py b/doc/examples/plot_windowed_histogram.py index 1c57721b..7fb6fcc1 100644 --- a/doc/examples/plot_windowed_histogram.py +++ b/doc/examples/plot_windowed_histogram.py @@ -4,31 +4,29 @@ from __future__ import division Sliding window histogram ======================== -Histogram matching can be used for object detection in images [1]_. -This example extracts a single coin from the `skimage.data.coins` image -and uses histogram matching to attempt to locate it within the original -image. +Histogram matching can be used for object detection in images [1]_. This +example extracts a single coin from the `skimage.data.coins` image and uses +histogram matching to attempt to locate it within the original image. First, a box-shaped region of the image containing the target coin is extracted and a histogram of its greyscale values is computed. -Next, for each pixel in the test image, a histogram of the greyscale values -in a region of the image surrounding the pixel is computed. -`skimage.filter.rank.windowed_histogram` is used for this task, as it -employs an efficient sliding window based algorithm that is able to compute -these histograms quickly [2]_. -The local histogram for the region surrounding each pixel in the image is -compared to that of the single coin, with a similarity measure being -computed and displayed. +Next, for each pixel in the test image, a histogram of the greyscale values in +a region of the image surrounding the pixel is computed. +`skimage.filter.rank.windowed_histogram` is used for this task, as it employs +an efficient sliding window based algorithm that is able to compute these +histograms quickly [2]_. The local histogram for the region surrounding each +pixel in the image is compared to that of the single coin, with a similarity +measure being computed and displayed. -The histogram of the single coin is computed using `numpy.histogram` on a -box shaped region surrounding the coin, while the sliding window histograms -are computed using a disc shaped structural element of a slightly different -size. This is done in aid of demonstrating that the technique still finds -similarity in spite of these differences. +The histogram of the single coin is computed using `numpy.histogram` on a box +shaped region surrounding the coin, while the sliding window histograms are +computed using a disc shaped structural element of a slightly different size. +This is done in aid of demonstrating that the technique still finds similarity +in spite of these differences. -To demonstrate the rotational invariance of the technique, the same -test is performed on a version of the coins image rotated by 45 degrees. +To demonstrate the rotational invariance of the technique, the same test is +performed on a version of the coins image rotated by 45 degrees. References ---------- @@ -41,11 +39,10 @@ import numpy as np import matplotlib import matplotlib.pyplot as plt -from skimage import data +from skimage import data, transform from skimage.util import img_as_ubyte from skimage.morphology import disk from skimage.filter import rank -from skimage import transform matplotlib.rcParams['font.size'] = 9 @@ -63,11 +60,11 @@ def windowed_histogram_similarity(image, selem, reference_hist, n_bins): # a measure of distance between histograms X = px_histograms Y = reference_hist - num = (X-Y)*(X-Y) - denom = X+Y + num = (X - Y) ** 2 + denom = X + Y frac = num / denom frac[denom == 0] = 0 - chi_sqr = np.sum(frac, axis=2) * 0.5 + chi_sqr = 0.5 * np.sum(frac, axis=2) # Generate a similarity measure. It needs to be low when distance is high # and high when distance is low; taking the reciprocal will do this. @@ -82,7 +79,7 @@ img = img_as_ubyte(data.coins()) # Quantize to 16 levels of greyscale; this way the output image will have a # 16-dimensional feature vector per pixel -quantized_img = img//16 +quantized_img = img // 16 # Select the coin from the 4th column, second row. # Co-ordinate ordering: [x1,y1,x2,y2] @@ -96,8 +93,8 @@ coin_hist = coin_hist.astype(float) / np.sum(coin_hist) # Compute a disk shaped mask that will define the shape of our sliding window -# Example coin is ~44px across, so make a disk 61px wide (2*rad+1) to be big -# enough for other coins too. +# Example coin is ~44px across, so make a disk 61px wide (2 * rad + 1) to be +# big enough for other coins too. selem = disk(30) @@ -108,39 +105,31 @@ similarity = windowed_histogram_similarity(quantized_img, selem, coin_hist, # Now try a rotated image rotated_img = img_as_ubyte(transform.rotate(img, 45.0, resize=True)) # Quantize to 16 levels as before -quantized_rotated_image = rotated_img//16 +quantized_rotated_image = rotated_img // 16 # Similarity on rotated image rotated_similarity = windowed_histogram_similarity(quantized_rotated_image, selem, coin_hist, coin_hist.shape[0]) -# Plot it all -fig, axes = plt.subplots(nrows=5, figsize=(6, 18)) -ax0, ax1, ax2, ax3, ax4 = axes +fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(10, 10)) -ax0.imshow(img, cmap='gray') -ax0.set_title('Original image') -ax0.axis('off') +axes[0, 0].imshow(quantized_img, cmap='gray') +axes[0, 0].set_title('Quantized image') +axes[0, 0].axis('off') -ax1.imshow(quantized_img, cmap='gray') -ax1.set_title('Quantized image') -ax1.axis('off') +axes[0, 1].imshow(coin, cmap='gray') +axes[0, 1].set_title('Coin from 2nd row, 4th column') +axes[0, 1].axis('off') -ax2.imshow(coin, cmap='gray') -ax2.set_title('Coin from 2nd row, 4th column') -ax2.axis('off') +axes[1, 0].imshow(img, cmap='gray') +axes[1, 0].imshow(similarity, cmap='jet', alpha=0.5) +axes[1, 0].set_title('Original image with overlayed similarity') +axes[1, 0].axis('off') -ax3.imshow(img, cmap='gray') -# While jet is not a great colormap, it makes the high similarity areas -# stand out -ax3.imshow(similarity, cmap='jet', alpha=0.5) -ax3.set_title('Original image with overlayed similarity') -ax3.axis('off') - -ax4.imshow(rotated_img, cmap='gray') -ax4.imshow(rotated_similarity, cmap='jet', alpha=0.5) -ax4.set_title('Rotated image with overlayed similarity') -ax4.axis('off') +axes[1, 1].imshow(rotated_img, cmap='gray') +axes[1, 1].imshow(rotated_similarity, cmap='jet', alpha=0.5) +axes[1, 1].set_title('Rotated image with overlayed similarity') +axes[1, 1].axis('off') plt.show() From 4c3c49558d43f37c051e61f59e1480ac095497dd Mon Sep 17 00:00:00 2001 From: capitanbatata Date: Wed, 3 Sep 2014 09:13:34 +0200 Subject: [PATCH 0330/1122] Corrected code to conform to Python coding conventions (PEP 8). Added test code for handling exceptions. Corrected test code to use the full path to the test data. --- skimage/__init__.py | 1 - skimage/color/colorconv.py | 74 +++++++++++++-------------- skimage/color/tests/test_colorconv.py | 39 +++++++++----- 3 files changed, 62 insertions(+), 52 deletions(-) diff --git a/skimage/__init__.py b/skimage/__init__.py index 59c80ed2..b3f923db 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -71,7 +71,6 @@ except ImportError: __version__ = "unbuilt-dev" del version - try: _imp.find_module('nose') except ImportError: diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 46268bd2..69c95a8c 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -321,7 +321,7 @@ gray_from_rgb = np.array([[0.2125, 0.7154, 0.0721], [0, 0, 0], [0, 0, 0]]) -# CIE LAB constants for Observer= 2A, Illuminant= D65 +# CIE LAB constants for Observer=2A, Illuminant=D65 ## NOTE: this is actually the XYZ values for the illuminant above. lab_ref_white = np.array([0.95047, 1., 1.08883]) @@ -348,35 +348,35 @@ lab_ref_white = np.array([0.95047, 1., 1.08883]) ## .. [1] http://en.wikipedia.org/wiki/Standard_illuminant illuminants = \ - {"A": [[1.098466069456375, 1, 0.3558228003436005], \ - [1.111420406956693, 1, 0.3519978321919493]], \ - "D50": [[0.9642119944211994, 1, 0.8251882845188288], \ - [0.9672062750333777, 1, 0.8142801513128616]], \ - "D55": [[0.956797052643698, 1, 0.9214805860173273], \ - [0.9579665682254781, 1, 0.9092525159847462]], \ - "D65": [lab_ref_white, \ - [0.94809667673716, 1, 1.0730513595166162]], \ - "D75": [[0.9497220898840717, 1, 1.226393520724154], \ - [0.9441713925645873, 1, 1.2064272211720228]], \ - "E": [[1.0, 1.0, 1.0], \ - [1.0, 1.0, 1.0]] + {"A": [(1.098466069456375, 1, 0.3558228003436005), \ + (1.111420406956693, 1, 0.3519978321919493)], \ + "D50": [(0.9642119944211994, 1, 0.8251882845188288), \ + (0.9672062750333777, 1, 0.8142801513128616)], \ + "D55": [(0.956797052643698, 1, 0.9214805860173273), \ + (0.9579665682254781, 1, 0.9092525159847462)], \ + "D65": [(0.95047, 1., 1.08883), # This was: `lab_ref_white` + (0.94809667673716, 1, 1.0730513595166162)], \ + "D75": [(0.9497220898840717, 1, 1.226393520724154), \ + (0.9441713925645873, 1, 1.2064272211720228)], \ + "E": [(1.0, 1.0, 1.0), \ + (1.0, 1.0, 1.0)] } def get_xyz_coords(illuminant, observer): - """Get the XYZ coordinates of the given illuminant and observer [1]. Currently + """Get the XYZ coordinates of the given illuminant and observer [1]_. Currently supported illuminants are: "A", "D50", "D55", "D65", "D75", "E". Parameters ---------- - illuminant: string + illuminant : {'A', 'D50', 'D55', 'D65', 'D75', 'E'}, optional The name of the illuminant (the function is NOT case sensitive). - observer: int + observer : int, optional The aperture angle of the observer. Returns ------- - xyz_coords: list - A list with 3 elements containing the XYZ coordinates of the given + (x, y, z) : tuple + A tuple with 3 elements containing the XYZ coordinates of the given illuminant. Raises @@ -391,17 +391,17 @@ def get_xyz_coords(illuminant, observer): """ illuminant = illuminant.upper() - if illuminant in illuminants.keys(): + if illuminant in illuminants: idx = 100; if observer == 2: idx = 0 elif observer == 10: idx = 1 else: - raise ValueError("Unknown observer \"{}\"".format(observer)) + raise ValueError("Unknown observer \"{0}\"".format(observer)) return illuminants[illuminant][idx] else: - raise ValueError("Unknown illuminant \"{}\"".format(illuminant)) + raise ValueError("Unknown illuminant \"{0}\"".format(illuminant)) # Haematoxylin-Eosin-DAB colorspace @@ -746,7 +746,7 @@ def gray2rgb(image): else: raise ValueError("Input image expected to be RGB, RGBA or gray.") -def xyz2lab(xyz, illuminant = "D65", observer = 2): +def xyz2lab(xyz, illuminant="D65", observer=2): """XYZ to CIE-LAB color space conversion. Parameters @@ -754,9 +754,9 @@ def xyz2lab(xyz, illuminant = "D65", observer = 2): xyz : array_like The image in XYZ format, in a 3- or 4-D array of shape ``(.., ..,[ ..,] 3)``. - illuminant: string + illuminant : {'A', 'D50', 'D55', 'D65', 'D75', 'E'}, optional The name of the illuminant (the function is NOT case sensitive). - observer: int + observer : int, optional The aperture angle of the observer. Returns @@ -775,8 +775,8 @@ def xyz2lab(xyz, illuminant = "D65", observer = 2): Notes ----- - By default Observer= 2A, Illuminant= D65. CIE XYZ tristimulus values x_ref - = 95.047, y_ref = 100., z_ref = 108.883. See function 'get_xyz_coords' for + By default Observer= 2A, Illuminant= D65. CIE XYZ tristimulus values + x_ref=95.047, y_ref=100., z_ref=108.883. See function `get_xyz_coords` for a list of supported illuminants. References @@ -814,16 +814,16 @@ def xyz2lab(xyz, illuminant = "D65", observer = 2): return np.concatenate([x[..., np.newaxis] for x in [L, a, b]], axis=-1) -def lab2xyz(lab, illuminant = "D65", observer = 2): +def lab2xyz(lab, illuminant="D65", observer=2): """CIE-LAB to XYZcolor space conversion. Parameters ---------- lab : array_like The image in lab format, in a 3-D array of shape ``(.., .., 3)``. - illuminant: string + illuminant : {'A', 'D50', 'D55', 'D65', 'D75', 'E'}, optional The name of the illuminant (the function is NOT case sensitive). - observer: int + observer : int, optional The aperture angle of the observer. Returns @@ -923,7 +923,7 @@ def lab2rgb(lab): return xyz2rgb(lab2xyz(lab)) -def xyz2luv(xyz, illuminant = "D65", observer = 2): +def xyz2luv(xyz, illuminant="D65", observer=2): """XYZ to CIE-Luv color space conversion. Parameters @@ -931,9 +931,9 @@ def xyz2luv(xyz, illuminant = "D65", observer = 2): xyz : (M, N, [P,] 3) array_like The 3 or 4 dimensional image in XYZ format. Final dimension denotes channels. - illuminant: string + illuminant : {'A', 'D50', 'D55', 'D65', 'D75', 'E'}, optional The name of the illuminant (the function is NOT case sensitive). - observer: int + observer : int, optional The aperture angle of the observer. Returns @@ -951,7 +951,7 @@ def xyz2luv(xyz, illuminant = "D65", observer = 2): Notes ----- - By default XYZ conversion weights use Observer = 2A. Reference whitepoint + By default XYZ conversion weights use observer=2A. Reference whitepoint for D65 Illuminant, with XYZ tristimulus values of ``(95.047, 100., 108.883)``. See function 'get_xyz_coords' for a list of supported illuminants. @@ -1000,7 +1000,7 @@ def xyz2luv(xyz, illuminant = "D65", observer = 2): return np.concatenate([q[..., np.newaxis] for q in [L, u, v]], axis=-1) -def luv2xyz(luv, illuminant = "D65", observer = 2): +def luv2xyz(luv, illuminant="D65", observer=2): """CIE-Luv to XYZ color space conversion. Parameters @@ -1008,9 +1008,9 @@ def luv2xyz(luv, illuminant = "D65", observer = 2): luv : (M, N, [P,] 3) array_like The 3 or 4 dimensional image in CIE-Luv format. Final dimension denotes channels. - illuminant: string + illuminant : {'A', 'D50', 'D55', 'D65', 'D75', 'E'}, optional The name of the illuminant (the function is NOT case sensitive). - observer: int + observer : int, optional The aperture angle of the observer. Returns @@ -1028,7 +1028,7 @@ def luv2xyz(luv, illuminant = "D65", observer = 2): Notes ----- - XYZ conversion weights use Observer = 2A. Reference whitepoint for D65 + XYZ conversion weights use observer=2A. Reference whitepoint for D65 Illuminant, with XYZ tristimulus values of ``(95.047, 100., 108.883)``. See function 'get_xyz_coords' for a list of supported illuminants. diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index 0c0fe507..7a0f0afb 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -233,18 +233,18 @@ class TestColorconv(TestCase): assert_array_almost_equal(xyz2lab(self.xyz_array), self.lab_array, decimal=3) - ## Thest the conversion with the rest of the illuminants. + ## Test the conversion with the rest of the illuminants. for I in ["d50", "d55", "d65", "d75"]: for obs in [2, 10]: print("testing illuminant={0}, observer={1}".format(I, obs)) fname = "lab_array_{0}_{1}.npy".format(I, obs) - lab_array_I_obs = np.load(os.path.join('data', fname)) + lab_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(lab_array_I_obs, xyz2lab(self.xyz_array, I, obs), decimal=2) for I in ["a", "e"]: print("testing illuminant={0}, observer=2".format(I)) fname = "lab_array_{0}_2.npy".format(I) - lab_array_I_obs = np.load(os.path.join('data', fname)) + lab_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(lab_array_I_obs, xyz2lab(self.xyz_array, I, 2), decimal=2) @@ -252,19 +252,30 @@ class TestColorconv(TestCase): assert_array_almost_equal(lab2xyz(self.lab_array), self.xyz_array, decimal=3) - ## Thest the conversion with the rest of the illuminants. + ## Test the conversion with the rest of the illuminants. for I in ["d50", "d55", "d65", "d75"]: for obs in [2, 10]: fname = "lab_array_{0}_{1}.npy".format(I, obs) - lab_array_I_obs = np.load(os.path.join('data', fname)) - assert_array_almost_equal(lab2xyz(lab_array_I_obs, I, obs), + lab_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) + assert_array_almost_equal(lab2xyz(lab_array_I_obs, I, obs), self.xyz_array, decimal=3) for I in ["a", "e"]: fname = "lab_array_{0}_2.npy".format(I, obs) - lab_array_I_obs = np.load(os.path.join('data', fname)) + lab_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(lab2xyz(lab_array_I_obs, I, 2), self.xyz_array, decimal=3) - + + ## And we include a call to test the exception handling in the code. + try: + xs = lab2xyz(lab_array_I_obs, "NaI", 2) # Not an illuminant + except ValueError: + print 'Correctly handled the unknown illuminant case.' + + try: + xs = lab2xyz(lab_array_I_obs, "d50", 42) # Not an illuminant + except ValueError: + print 'Correctly handled the unknown observer case.' + def test_rgb2lab_brucelindbloom(self): """ @@ -296,18 +307,18 @@ class TestColorconv(TestCase): assert_array_almost_equal(xyz2luv(self.xyz_array), self.luv_array, decimal=3) - ## Thest the conversion with the rest of the illuminants. + ## Test the conversion with the rest of the illuminants. for I in ["d50", "d55", "d65", "d75"]: for obs in [2, 10]: print("testing illuminant={0}, observer={1}".format(I, obs)) fname = "luv_array_{0}_{1}.npy".format(I, obs) - luv_array_I_obs = np.load(os.path.join('data', fname)) + luv_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(luv_array_I_obs, xyz2luv(self.xyz_array, I, obs), decimal=2) for I in ["a", "e"]: print("testing illuminant={0}, observer=2".format(I)) fname = "luv_array_{0}_2.npy".format(I) - luv_array_I_obs = np.load(os.path.join('data', fname)) + luv_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(luv_array_I_obs, xyz2luv(self.xyz_array, I, 2), decimal=2) @@ -316,16 +327,16 @@ class TestColorconv(TestCase): assert_array_almost_equal(luv2xyz(self.luv_array), self.xyz_array, decimal=3) - ## Thest the conversion with the rest of the illuminants. + ## Test the conversion with the rest of the illuminants. for I in ["d50", "d55", "d65", "d75"]: for obs in [2, 10]: fname = "luv_array_{0}_{1}.npy".format(I, obs) - luv_array_I_obs = np.load(os.path.join('data', fname)) + luv_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(luv2xyz(luv_array_I_obs, I, obs), self.xyz_array, decimal=3) for I in ["a", "e"]: fname = "luv_array_{0}_2.npy".format(I, obs) - luv_array_I_obs = np.load(os.path.join('data', fname)) + luv_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(luv2xyz(luv_array_I_obs, I, 2), self.xyz_array, decimal=3) From 90b2755b015a6ef563ca104e7381866462e4de3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 3 Sep 2014 12:54:45 -0400 Subject: [PATCH 0331/1122] Fix spelling --- doc/examples/plot_windowed_histogram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/examples/plot_windowed_histogram.py b/doc/examples/plot_windowed_histogram.py index 7fb6fcc1..b6019465 100644 --- a/doc/examples/plot_windowed_histogram.py +++ b/doc/examples/plot_windowed_histogram.py @@ -124,12 +124,12 @@ axes[0, 1].axis('off') axes[1, 0].imshow(img, cmap='gray') axes[1, 0].imshow(similarity, cmap='jet', alpha=0.5) -axes[1, 0].set_title('Original image with overlayed similarity') +axes[1, 0].set_title('Original image with overlaid similarity') axes[1, 0].axis('off') axes[1, 1].imshow(rotated_img, cmap='gray') axes[1, 1].imshow(rotated_similarity, cmap='jet', alpha=0.5) -axes[1, 1].set_title('Rotated image with overlayed similarity') +axes[1, 1].set_title('Rotated image with overlaid similarity') axes[1, 1].axis('off') plt.show() From c4bb726ff64761f4d3c93ad53231b8fd394a2370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 3 Sep 2014 12:55:07 -0400 Subject: [PATCH 0332/1122] Replace jet with hot colormap --- doc/examples/plot_windowed_histogram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/examples/plot_windowed_histogram.py b/doc/examples/plot_windowed_histogram.py index b6019465..4d51c3e8 100644 --- a/doc/examples/plot_windowed_histogram.py +++ b/doc/examples/plot_windowed_histogram.py @@ -123,12 +123,12 @@ axes[0, 1].set_title('Coin from 2nd row, 4th column') axes[0, 1].axis('off') axes[1, 0].imshow(img, cmap='gray') -axes[1, 0].imshow(similarity, cmap='jet', alpha=0.5) +axes[1, 0].imshow(similarity, cmap='hot', alpha=0.5) axes[1, 0].set_title('Original image with overlaid similarity') axes[1, 0].axis('off') axes[1, 1].imshow(rotated_img, cmap='gray') -axes[1, 1].imshow(rotated_similarity, cmap='jet', alpha=0.5) +axes[1, 1].imshow(rotated_similarity, cmap='hot', alpha=0.5) axes[1, 1].set_title('Rotated image with overlaid similarity') axes[1, 1].axis('off') From acbee6b5164659b16d0d5db4b2fc28220bad35d8 Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Wed, 3 Sep 2014 17:54:08 -0700 Subject: [PATCH 0333/1122] transform: Remove some leftovers in __all__ 5c9d7af removed a bunch of deprecated functions, but forgot to remove two more names from __all__. Yes, I know * imports are a bad idea, but we have an internal app that uses them. I've removed the * imports, but figured I might as well fix upstream. --- skimage/transform/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 877f1614..3049ebb0 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -18,8 +18,6 @@ __all__ = ['hough_circle', 'hough_ellipse', 'hough_line', 'probabilistic_hough_line', - 'probabilistic_hough', - 'hough_peaks', 'hough_line_peaks', 'radon', 'iradon', From 051c54c577b32f76f88fcd7d894767bc48b81bad Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Thu, 4 Sep 2014 21:29:02 +0100 Subject: [PATCH 0334/1122] Fix indexing bug in freeimage plugin --- skimage/io/_plugins/freeimage_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/io/_plugins/freeimage_plugin.py b/skimage/io/_plugins/freeimage_plugin.py index c3bd4e73..187264e6 100644 --- a/skimage/io/_plugins/freeimage_plugin.py +++ b/skimage/io/_plugins/freeimage_plugin.py @@ -672,7 +672,7 @@ def _array_to_bitmap(array): raise RuntimeError('Could not allocate image for storage') try: def n(arr): # normalise to freeimage's in-memory format - return arr.T[:, ::-1] + return arr.T[..., ::-1] wrapped_array = _wrap_bitmap_bits_in_array(bitmap, w_shape, dtype) # swizzle the color components and flip the scanlines to go to From 84f3835106aaeb54293c368e30075f90c8a966bc Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Thu, 4 Sep 2014 22:32:02 +0100 Subject: [PATCH 0335/1122] Fix Big Endian check --- skimage/io/_plugins/freeimage_plugin.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/skimage/io/_plugins/freeimage_plugin.py b/skimage/io/_plugins/freeimage_plugin.py index 187264e6..79c97641 100644 --- a/skimage/io/_plugins/freeimage_plugin.py +++ b/skimage/io/_plugins/freeimage_plugin.py @@ -677,24 +677,23 @@ def _array_to_bitmap(array): wrapped_array = _wrap_bitmap_bits_in_array(bitmap, w_shape, dtype) # swizzle the color components and flip the scanlines to go to # FreeImage's BGR[A] and upside-down internal memory format - if len(shape) == 3: + if len(shape) == 3 and _FI.FreeImage_IsLittleEndian(): R = array[:, :, 0] G = array[:, :, 1] B = array[:, :, 2] - if _FI.FreeImage_IsLittleEndian(): - if dtype.type == numpy.uint8: - wrapped_array[0] = n(B) - wrapped_array[1] = n(G) - wrapped_array[2] = n(R) - elif dtype.type == numpy.uint16: - wrapped_array[0] = n(R) - wrapped_array[1] = n(G) - wrapped_array[2] = n(B) + if dtype.type == numpy.uint8: + wrapped_array[0] = n(B) + wrapped_array[1] = n(G) + wrapped_array[2] = n(R) + elif dtype.type == numpy.uint16: + wrapped_array[0] = n(R) + wrapped_array[1] = n(G) + wrapped_array[2] = n(B) - if shape[2] == 4: - A = array[:, :, 3] - wrapped_array[3] = n(A) + if shape[2] == 4: + A = array[:, :, 3] + wrapped_array[3] = n(A) else: wrapped_array[:] = n(array) if len(shape) == 2 and dtype.type == numpy.uint8: From 5920d6788d560e6d1bd216fbbd08f13eca670615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Thu, 4 Sep 2014 18:00:56 -0400 Subject: [PATCH 0336/1122] simplify notations --- doc/source/user_guide/parallelization.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/source/user_guide/parallelization.txt b/doc/source/user_guide/parallelization.txt index f0323c7d..e083cbd2 100755 --- a/doc/source/user_guide/parallelization.txt +++ b/doc/source/user_guide/parallelization.txt @@ -15,7 +15,7 @@ on a large batch of images. Here is an example: """ Apply some functions and return an image. """ - image = denoise_tv_chambolle(image, weight=0.1, multichannel=True) + image = denoise_tv_chambolle(image[0][0], weight=0.1, multichannel=True) fd, hog_image = hog(color.rgb2gray(image), orientations=8, pixels_per_cell=(16, 16), cells_per_block=(1, 1), visualise=True) @@ -35,7 +35,7 @@ use ipython and measure the execution time with ``%timeit``. def classic_loop(): for image in pics: - task(image[0][0]) + task(image) %timeit classic_loop() @@ -45,7 +45,7 @@ Another equivalent way to code this loop is to use a comprehension list which ha .. code-block:: python def comprehension_loop(): - [task(image[0][0]) for image in pics] + [task(image) for image in pics] %timeit comprehension_loop() @@ -56,6 +56,6 @@ The number of jobs can be specified. from joblib import Parallel, delayed def joblib_loop(): - Parallel(n_jobs=4)(delayed(task)(i[0][0]) for i in pics) + Parallel(n_jobs=4)(delayed(task)(i) for i in pics) %timeit joblib_loop() From 272e9310d2bacc8b9a9534a28b421572c0bbd200 Mon Sep 17 00:00:00 2001 From: capitanbatata Date: Fri, 5 Sep 2014 13:35:35 +0200 Subject: [PATCH 0337/1122] Added Stephan comments. Syntax is now checked using `flake8`. --- skimage/__init__.py | 1 + skimage/color/colorconv.py | 147 +++++++++++++------------- skimage/color/tests/test_colorconv.py | 50 ++++----- 3 files changed, 100 insertions(+), 98 deletions(-) diff --git a/skimage/__init__.py b/skimage/__init__.py index b3f923db..59c80ed2 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -71,6 +71,7 @@ except ImportError: __version__ = "unbuilt-dev" del version + try: _imp.find_module('nose') except ImportError: diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 69c95a8c..53642b1f 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -56,7 +56,7 @@ from __future__ import division import numpy as np from scipy import linalg from ..util import dtype -from skimage._shared.utils import deprecated +from skimage._shared.utils import deprecated # Imported but unused. Remove? def guess_spatial_dimensions(image): @@ -114,7 +114,8 @@ def convert_colorspace(arr, fromspace, tospace): Notes ----- Conversion occurs through the "central" RGB color space, i.e. conversion - from XYZ to HSV is implemented as ``XYZ -> RGB -> HSV`` instead of directly. + from XYZ to HSV is implemented as ``XYZ -> RGB -> HSV`` instead of + directly. Examples -------- @@ -129,9 +130,9 @@ def convert_colorspace(arr, fromspace, tospace): fromspace = fromspace.upper() tospace = tospace.upper() - if not fromspace in fromdict.keys(): + if fromspace not in fromdict.keys(): raise ValueError('fromspace needs to be one of %s' % fromdict.keys()) - if not tospace in todict.keys(): + if tospace not in todict.keys(): raise ValueError('tospace needs to be one of %s' % todict.keys()) return todict[tospace](fromdict[fromspace](arr)) @@ -287,15 +288,15 @@ def hsv2rgb(hsv): return out -#--------------------------------------------------------------- +# --------------------------------------------------------------- # Primaries for the coordinate systems -#--------------------------------------------------------------- +# --------------------------------------------------------------- cie_primaries = np.array([700, 546.1, 435.8]) sb_primaries = np.array([1. / 155, 1. / 190, 1. / 225]) * 1e5 -#--------------------------------------------------------------- +# --------------------------------------------------------------- # Matrices that define conversion between different color spaces -#--------------------------------------------------------------- +# --------------------------------------------------------------- # From sRGB specification xyz_from_rgb = np.array([[0.412453, 0.357580, 0.180423], @@ -322,49 +323,49 @@ gray_from_rgb = np.array([[0.2125, 0.7154, 0.0721], [0, 0, 0]]) # CIE LAB constants for Observer=2A, Illuminant=D65 -## NOTE: this is actually the XYZ values for the illuminant above. +# NOTE: this is actually the XYZ values for the illuminant above. lab_ref_white = np.array([0.95047, 1., 1.08883]) -## XYZ coordinates of the illuminants, scaled to [0, 1]. For each illuminant I we have: -## -## illuminant[I][0] corresponds to the XYZ coordinates for the 2 degree -## field of view. -## -## illuminant[I][1] corresponds to the XYZ coordinates for the 10 degree -## field of view. -## -## The XYZ coordinates are calculated from [1], using the formula: -## -## X = x * ( Y / y ) -## Y = Y -## Z = ( 1 - x - y ) * ( Y / y ) -## -## where Y = 1. The only exception is the illuminant "D65" with aperture angle -## 2, whose coordinates are copied from 'lab_ref_white' for -## backward-compatibility reasons. -## -## References -## ---------- -## .. [1] http://en.wikipedia.org/wiki/Standard_illuminant +# XYZ coordinates of the illuminants, scaled to [0, 1]. For each illuminant I +# we have: +# +# illuminant[I][0] corresponds to the XYZ coordinates for the 2 degree +# field of view. +# +# illuminant[I][1] corresponds to the XYZ coordinates for the 10 degree +# field of view. +# +# The XYZ coordinates are calculated from [1], using the formula: +# +# X = x * ( Y / y ) +# Y = Y +# Z = ( 1 - x - y ) * ( Y / y ) +# +# where Y = 1. The only exception is the illuminant "D65" with aperture angle +# 2, whose coordinates are copied from 'lab_ref_white' for +# backward-compatibility reasons. +# +# References +# ---------- +# .. [1] http://en.wikipedia.org/wiki/Standard_illuminant illuminants = \ - {"A": [(1.098466069456375, 1, 0.3558228003436005), \ - (1.111420406956693, 1, 0.3519978321919493)], \ - "D50": [(0.9642119944211994, 1, 0.8251882845188288), \ - (0.9672062750333777, 1, 0.8142801513128616)], \ - "D55": [(0.956797052643698, 1, 0.9214805860173273), \ - (0.9579665682254781, 1, 0.9092525159847462)], \ - "D65": [(0.95047, 1., 1.08883), # This was: `lab_ref_white` - (0.94809667673716, 1, 1.0730513595166162)], \ - "D75": [(0.9497220898840717, 1, 1.226393520724154), \ - (0.9441713925645873, 1, 1.2064272211720228)], \ - "E": [(1.0, 1.0, 1.0), \ - (1.0, 1.0, 1.0)] - } + {"A": {'2': (1.098466069456375, 1, 0.3558228003436005), + '10': (1.111420406956693, 1, 0.3519978321919493)}, + "D50": {'2': (0.9642119944211994, 1, 0.8251882845188288), + '10': (0.9672062750333777, 1, 0.8142801513128616)}, + "D55": {'2': (0.956797052643698, 1, 0.9214805860173273), + '10': (0.9579665682254781, 1, 0.9092525159847462)}, + "D65": {'2': (0.95047, 1., 1.08883), # This was: `lab_ref_white` + '10': (0.94809667673716, 1, 1.0730513595166162)}, + "D75": {'2': (0.9497220898840717, 1, 1.226393520724154), + '10': (0.9441713925645873, 1, 1.2064272211720228)}, + "E": {'2': (1.0, 1.0, 1.0), + '10': (1.0, 1.0, 1.0)}} + def get_xyz_coords(illuminant, observer): - """Get the XYZ coordinates of the given illuminant and observer [1]_. Currently - supported illuminants are: "A", "D50", "D55", "D65", "D75", "E". + """Get the XYZ coordinates of the given illuminant and observer [1]_. Parameters ---------- @@ -391,18 +392,11 @@ def get_xyz_coords(illuminant, observer): """ illuminant = illuminant.upper() - if illuminant in illuminants: - idx = 100; - if observer == 2: - idx = 0 - elif observer == 10: - idx = 1 - else: - raise ValueError("Unknown observer \"{0}\"".format(observer)) - return illuminants[illuminant][idx] - else: - raise ValueError("Unknown illuminant \"{0}\"".format(illuminant)) - + try: + return illuminants[illuminant][observer] + except KeyError: + raise ValueError("Unknown illuminant/observer combination\ + (\"{0}\", \"{1}\")".format(illuminant, observer)) # Haematoxylin-Eosin-DAB colorspace # From original Ruifrok's paper: A. C. Ruifrok and D. A. Johnston, @@ -487,9 +481,9 @@ rgb_from_hpx = np.array([[0.644211, 0.716556, 0.266844], rgb_from_hpx[2, :] = np.cross(rgb_from_hpx[0, :], rgb_from_hpx[1, :]) hpx_from_rgb = linalg.inv(rgb_from_hpx) -#------------------------------------------------------------- +# ------------------------------------------------------------- # The conversion functions that make use of the matrices above -#------------------------------------------------------------- +# ------------------------------------------------------------- def _convert(matrix, arr): @@ -745,8 +739,9 @@ def gray2rgb(image): return np.concatenate(3 * (image,), axis=-1) else: raise ValueError("Input image expected to be RGB, RGBA or gray.") - -def xyz2lab(xyz, illuminant="D65", observer=2): + + +def xyz2lab(xyz, illuminant="D65", observer="2"): """XYZ to CIE-LAB color space conversion. Parameters @@ -756,7 +751,7 @@ def xyz2lab(xyz, illuminant="D65", observer=2): ``(.., ..,[ ..,] 3)``. illuminant : {'A', 'D50', 'D55', 'D65', 'D75', 'E'}, optional The name of the illuminant (the function is NOT case sensitive). - observer : int, optional + observer : {"2", "10"}, optional The aperture angle of the observer. Returns @@ -796,7 +791,7 @@ def xyz2lab(xyz, illuminant="D65", observer=2): arr = _prepare_colorarray(xyz) xyz_ref_white = get_xyz_coords(illuminant, observer) - + # scale by CIE XYZ tristimulus values of the reference white point arr = arr / xyz_ref_white @@ -814,7 +809,8 @@ def xyz2lab(xyz, illuminant="D65", observer=2): return np.concatenate([x[..., np.newaxis] for x in [L, a, b]], axis=-1) -def lab2xyz(lab, illuminant="D65", observer=2): + +def lab2xyz(lab, illuminant="D65", observer="2"): """CIE-LAB to XYZcolor space conversion. Parameters @@ -823,7 +819,7 @@ def lab2xyz(lab, illuminant="D65", observer=2): The image in lab format, in a 3-D array of shape ``(.., .., 3)``. illuminant : {'A', 'D50', 'D55', 'D65', 'D75', 'E'}, optional The name of the illuminant (the function is NOT case sensitive). - observer : int, optional + observer : {"2", "10"}, optional The aperture angle of the observer. Returns @@ -871,6 +867,7 @@ def lab2xyz(lab, illuminant="D65", observer=2): out *= xyz_ref_white return out + def rgb2lab(rgb): """RGB to lab color space conversion. @@ -923,7 +920,7 @@ def lab2rgb(lab): return xyz2rgb(lab2xyz(lab)) -def xyz2luv(xyz, illuminant="D65", observer=2): +def xyz2luv(xyz, illuminant="D65", observer="2"): """XYZ to CIE-Luv color space conversion. Parameters @@ -933,7 +930,7 @@ def xyz2luv(xyz, illuminant="D65", observer=2): channels. illuminant : {'A', 'D50', 'D55', 'D65', 'D75', 'E'}, optional The name of the illuminant (the function is NOT case sensitive). - observer : int, optional + observer : {"2", "10"}, optional The aperture angle of the observer. Returns @@ -1000,7 +997,7 @@ def xyz2luv(xyz, illuminant="D65", observer=2): return np.concatenate([q[..., np.newaxis] for q in [L, u, v]], axis=-1) -def luv2xyz(luv, illuminant="D65", observer=2): +def luv2xyz(luv, illuminant="D65", observer="2"): """CIE-Luv to XYZ color space conversion. Parameters @@ -1010,7 +1007,7 @@ def luv2xyz(luv, illuminant="D65", observer=2): channels. illuminant : {'A', 'D50', 'D55', 'D65', 'D75', 'E'}, optional The name of the illuminant (the function is NOT case sensitive). - observer : int, optional + observer : {"2", "10"}, optional The aperture angle of the observer. Returns @@ -1164,7 +1161,8 @@ def hed2rgb(hed): Parameters ---------- hed : array_like - The image in the HED color space, in a 3-D array of shape ``(.., .., 3)``. + The image in the HED color space, in a 3-D array of shape + ``(.., .., 3)``. Returns ------- @@ -1207,7 +1205,8 @@ def separate_stains(rgb, conv_matrix): Returns ------- out : ndarray - The image in stain color space, in a 3-D array of shape ``(.., .., 3)``. + The image in stain color space, in a 3-D array of shape + ``(.., .., 3)``. Raises ------ @@ -1254,7 +1253,8 @@ def combine_stains(stains, conv_matrix): Parameters ---------- stains : array_like - The image in stain color space, in a 3-D array of shape ``(.., .., 3)``. + The image in stain color space, in a 3-D array of shape + ``(.., .., 3)``. conv_matrix: ndarray The stain separation matrix as described by G. Landini [1]_. @@ -1305,7 +1305,8 @@ def combine_stains(stains, conv_matrix): stains = dtype.img_as_float(stains) logrgb2 = np.dot(-np.reshape(stains, (-1, 3)), conv_matrix) rgb2 = np.exp(logrgb2) - return rescale_intensity(np.reshape(rgb2 - 2, stains.shape), in_range=(-1, 1)) + return rescale_intensity(np.reshape(rgb2 - 2, stains.shape), + in_range=(-1, 1)) def lab2lch(lab): diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index 7a0f0afb..cd591829 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -71,24 +71,24 @@ class TestColorconv(TestCase): colbars_point75 = colbars * 0.75 colbars_point75_array = np.swapaxes(colbars_point75.reshape(3, 4, 2), 0, 2) - xyz_array = np.array([[[0.4124, 0.21260, 0.01930]], # red - [[0, 0, 0]], # black - [[.9505, 1., 1.089]], # white - [[.1805, .0722, .9505]], # blue - [[.07719, .15438, .02573]], # green + xyz_array = np.array([[[0.4124, 0.21260, 0.01930]], # red + [[0, 0, 0]], # black + [[.9505, 1., 1.089]], # white + [[.1805, .0722, .9505]], # blue + [[.07719, .15438, .02573]], # green ]) - lab_array = np.array([[[53.233, 80.109, 67.220]], # red - [[0., 0., 0.]], # black - [[100.0, 0.005, -0.010]], # white - [[32.303, 79.197, -107.864]], # blue - [[46.229, -51.7, 49.898]], # green + lab_array = np.array([[[53.233, 80.109, 67.220]], # red + [[0., 0., 0.]], # black + [[100.0, 0.005, -0.010]], # white + [[32.303, 79.197, -107.864]], # blue + [[46.229, -51.7, 49.898]], # green ]) - luv_array = np.array([[[53.233, 175.053, 37.751]], # red - [[0., 0., 0.]], # black - [[100., 0.001, -0.017]], # white - [[32.303, -9.400, -130.358]], # blue - [[46.228, -43.774, 56.589]], # green + luv_array = np.array([[[53.233, 175.053, 37.751]], # red + [[0., 0., 0.]], # black + [[100., 0.001, -0.017]], # white + [[32.303, -9.400, -130.358]], # blue + [[46.228, -43.774, 56.589]], # green ]) # RGB to HSV @@ -235,7 +235,7 @@ class TestColorconv(TestCase): ## Test the conversion with the rest of the illuminants. for I in ["d50", "d55", "d65", "d75"]: - for obs in [2, 10]: + for obs in ["2", "10"]: print("testing illuminant={0}, observer={1}".format(I, obs)) fname = "lab_array_{0}_{1}.npy".format(I, obs) lab_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) @@ -246,7 +246,7 @@ class TestColorconv(TestCase): fname = "lab_array_{0}_2.npy".format(I) lab_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(lab_array_I_obs, - xyz2lab(self.xyz_array, I, 2), decimal=2) + xyz2lab(self.xyz_array, I, "2"), decimal=2) def test_lab2xyz(self): assert_array_almost_equal(lab2xyz(self.lab_array), @@ -254,7 +254,7 @@ class TestColorconv(TestCase): ## Test the conversion with the rest of the illuminants. for I in ["d50", "d55", "d65", "d75"]: - for obs in [2, 10]: + for obs in ["2", "10"]: fname = "lab_array_{0}_{1}.npy".format(I, obs) lab_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(lab2xyz(lab_array_I_obs, I, obs), @@ -262,17 +262,17 @@ class TestColorconv(TestCase): for I in ["a", "e"]: fname = "lab_array_{0}_2.npy".format(I, obs) lab_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) - assert_array_almost_equal(lab2xyz(lab_array_I_obs, I, 2), + assert_array_almost_equal(lab2xyz(lab_array_I_obs, I, "2"), self.xyz_array, decimal=3) ## And we include a call to test the exception handling in the code. try: - xs = lab2xyz(lab_array_I_obs, "NaI", 2) # Not an illuminant + xs = lab2xyz(lab_array_I_obs, "NaI", "2") # Not an illuminant except ValueError: print 'Correctly handled the unknown illuminant case.' try: - xs = lab2xyz(lab_array_I_obs, "d50", 42) # Not an illuminant + xs = lab2xyz(lab_array_I_obs, "d50", "42") # Not an illuminant except ValueError: print 'Correctly handled the unknown observer case.' @@ -309,7 +309,7 @@ class TestColorconv(TestCase): ## Test the conversion with the rest of the illuminants. for I in ["d50", "d55", "d65", "d75"]: - for obs in [2, 10]: + for obs in ["2", "10"]: print("testing illuminant={0}, observer={1}".format(I, obs)) fname = "luv_array_{0}_{1}.npy".format(I, obs) luv_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) @@ -320,7 +320,7 @@ class TestColorconv(TestCase): fname = "luv_array_{0}_2.npy".format(I) luv_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(luv_array_I_obs, - xyz2luv(self.xyz_array, I, 2), decimal=2) + xyz2luv(self.xyz_array, I, "2"), decimal=2) def test_luv2xyz(self): @@ -329,7 +329,7 @@ class TestColorconv(TestCase): ## Test the conversion with the rest of the illuminants. for I in ["d50", "d55", "d65", "d75"]: - for obs in [2, 10]: + for obs in ["2", "10"]: fname = "luv_array_{0}_{1}.npy".format(I, obs) luv_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(luv2xyz(luv_array_I_obs, I, obs), @@ -337,7 +337,7 @@ class TestColorconv(TestCase): for I in ["a", "e"]: fname = "luv_array_{0}_2.npy".format(I, obs) luv_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) - assert_array_almost_equal(luv2xyz(luv_array_I_obs, I, 2), + assert_array_almost_equal(luv2xyz(luv_array_I_obs, I, "2"), self.xyz_array, decimal=3) def test_rgb2luv_brucelindbloom(self): From 87c72ae60f2f471153c13f9624c4bcc4c5284053 Mon Sep 17 00:00:00 2001 From: Julian Taylor Date: Fri, 5 Sep 2014 23:12:46 +0200 Subject: [PATCH 0338/1122] BUG: add signed flag to chars Sign of char is undefined and there are numerous places where its sign is important. Fixes tests with -funsigned-char. Closes gh-1110 --- skimage/feature/_texture.pyx | 2 +- skimage/feature/corner_cy.pyx | 12 ++++----- skimage/feature/orb_cy.pyx | 8 +++--- skimage/filter/rank/bilateral_cy.pyx | 6 ++--- skimage/filter/rank/core_cy.pxd | 2 +- skimage/filter/rank/core_cy.pyx | 10 +++---- skimage/filter/rank/generic_cy.pyx | 38 +++++++++++++-------------- skimage/filter/rank/percentile_cy.pyx | 18 ++++++------- skimage/morphology/cmorph.pyx | 4 +-- 9 files changed, 50 insertions(+), 50 deletions(-) diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index 8d49b377..4ee23a7a 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -115,7 +115,7 @@ def _local_binary_pattern(double[:, ::1] image, # pre-allocate arrays for computation cdef double[::1] texture = np.zeros(P, dtype=np.double) - cdef char[::1] signed_texture = np.zeros(P, dtype=np.int8) + cdef signed char[::1] signed_texture = np.zeros(P, dtype=np.int8) cdef int[::1] rotation_chain = np.zeros(P, dtype=np.int32) output_shape = (image.shape[0], image.shape[1]) diff --git a/skimage/feature/corner_cy.pyx b/skimage/feature/corner_cy.pyx index 86ad3c43..7aef99e9 100644 --- a/skimage/feature/corner_cy.pyx +++ b/skimage/feature/corner_cy.pyx @@ -87,7 +87,7 @@ def corner_moravec(image, Py_ssize_t window_size=1): cdef inline double _corner_fast_response(double curr_pixel, double* circle_intensities, - char* bins, char state, char n): + signed char* bins, signed char state, char n): cdef char consecutive_count = 0 cdef double curr_response cdef Py_ssize_t l, m @@ -104,22 +104,22 @@ cdef inline double _corner_fast_response(double curr_pixel, return 0 -def _corner_fast(double[:, ::1] image, char n, double threshold): +def _corner_fast(double[:, ::1] image, signed char n, double threshold): cdef Py_ssize_t rows = image.shape[0] cdef Py_ssize_t cols = image.shape[1] cdef Py_ssize_t i, j, k - cdef char speed_sum_b, speed_sum_d + cdef signed char speed_sum_b, speed_sum_d cdef double curr_pixel cdef double lower_threshold, upper_threshold cdef double[:, ::1] corner_response = np.zeros((rows, cols), dtype=np.double) - cdef char *rp = [0, 1, 2, 3, 3, 3, 2, 1, 0, -1, -2, -3, -3, -3, -2, -1] - cdef char *cp = [3, 3, 2, 1, 0, -1, -2, -3, -3, -3, -2, -1, 0, 1, 2, 3] - cdef char bins[16] + cdef signed char *rp = [0, 1, 2, 3, 3, 3, 2, 1, 0, -1, -2, -3, -3, -3, -2, -1] + cdef signed char *cp = [3, 3, 2, 1, 0, -1, -2, -3, -3, -3, -2, -1, 0, 1, 2, 3] + cdef signed char bins[16] cdef double circle_intensities[16] cdef double curr_response diff --git a/skimage/feature/orb_cy.pyx b/skimage/feature/orb_cy.pyx index 6e0801f8..c5129e85 100644 --- a/skimage/feature/orb_cy.pyx +++ b/skimage/feature/orb_cy.pyx @@ -25,10 +25,10 @@ def _orb_loop(double[:, ::1] image, Py_ssize_t[:, ::1] keypoints, cdef Py_ssize_t i, d, kr, kc, pr0, pr1, pc0, pc1, spr0, spc0, spr1, spc1 cdef int[:, ::1] steered_pos0, steered_pos1 cdef double angle - cdef char[:, ::1] descriptors = np.zeros((keypoints.shape[0], - POS.shape[0]), dtype=np.uint8) - cdef char[:, ::1] cpos0 = POS0 - cdef char[:, ::1] cpos1 = POS1 + cdef unsigned char[:, ::1] descriptors = \ + np.zeros((keypoints.shape[0], POS.shape[0]), dtype=np.uint8) + cdef signed char[:, ::1] cpos0 = POS0 + cdef signed char[:, ::1] cpos1 = POS1 for i in range(descriptors.shape[0]): diff --git a/skimage/filter/rank/bilateral_cy.pyx b/skimage/filter/rank/bilateral_cy.pyx index b4e22d3f..3acc26a4 100644 --- a/skimage/filter/rank/bilateral_cy.pyx +++ b/skimage/filter/rank/bilateral_cy.pyx @@ -80,7 +80,7 @@ def _mean(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t s0, Py_ssize_t s1, + signed char shift_x, signed char shift_y, Py_ssize_t s0, Py_ssize_t s1, Py_ssize_t max_bin): _core(_kernel_mean[dtype_t_out, dtype_t], image, selem, mask, out, @@ -91,7 +91,7 @@ def _pop(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t s0, Py_ssize_t s1, + signed char shift_x, signed char shift_y, Py_ssize_t s0, Py_ssize_t s1, Py_ssize_t max_bin): _core(_kernel_pop[dtype_t_out, dtype_t], image, selem, mask, out, @@ -102,7 +102,7 @@ def _sum(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t s0, Py_ssize_t s1, + signed char shift_x, signed char shift_y, Py_ssize_t s0, Py_ssize_t s1, Py_ssize_t max_bin): _core(_kernel_sum[dtype_t_out, dtype_t], image, selem, mask, out, diff --git a/skimage/filter/rank/core_cy.pxd b/skimage/filter/rank/core_cy.pxd index 9627c0d9..795f0819 100644 --- a/skimage/filter/rank/core_cy.pxd +++ b/skimage/filter/rank/core_cy.pxd @@ -22,7 +22,7 @@ cdef void _core(void kernel(dtype_t_out*, Py_ssize_t, Py_ssize_t*, double, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, + signed char shift_x, signed char shift_y, double p0, double p1, Py_ssize_t s0, Py_ssize_t s1, Py_ssize_t max_bin) except * diff --git a/skimage/filter/rank/core_cy.pyx b/skimage/filter/rank/core_cy.pyx index 4bd21df0..822c7251 100644 --- a/skimage/filter/rank/core_cy.pyx +++ b/skimage/filter/rank/core_cy.pyx @@ -49,7 +49,7 @@ cdef void _core(void kernel(dtype_t_out*, Py_ssize_t, Py_ssize_t*, double, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, + signed char shift_x, signed char shift_y, double p0, double p1, Py_ssize_t s0, Py_ssize_t s1, Py_ssize_t max_bin) except *: @@ -112,16 +112,16 @@ cdef void _core(void kernel(dtype_t_out*, Py_ssize_t, Py_ssize_t*, double, # build attack and release borders by using difference along axis t = np.hstack((selem, np.zeros((selem.shape[0], 1)))) - cdef char[:, :] t_e = (np.diff(t, axis=1) < 0).view(np.uint8) + cdef unsigned char[:, :] t_e = (np.diff(t, axis=1) < 0).view(np.uint8) t = np.hstack((np.zeros((selem.shape[0], 1)), selem)) - cdef char[:, :] t_w = (np.diff(t, axis=1) > 0).view(np.uint8) + cdef unsigned char[:, :] t_w = (np.diff(t, axis=1) > 0).view(np.uint8) t = np.vstack((selem, np.zeros((1, selem.shape[1])))) - cdef char[:, :] t_s = (np.diff(t, axis=0) < 0).view(np.uint8) + cdef unsigned char[:, :] t_s = (np.diff(t, axis=0) < 0).view(np.uint8) t = np.vstack((np.zeros((1, selem.shape[1])), selem)) - cdef char[:, :] t_n = (np.diff(t, axis=0) > 0).view(np.uint8) + cdef unsigned char[:, :] t_n = (np.diff(t, axis=0) > 0).view(np.uint8) for r in range(srows): for c in range(scols): diff --git a/skimage/filter/rank/generic_cy.pyx b/skimage/filter/rank/generic_cy.pyx index 98c8cd8a..a5c70bf4 100644 --- a/skimage/filter/rank/generic_cy.pyx +++ b/skimage/filter/rank/generic_cy.pyx @@ -411,7 +411,7 @@ def _autolevel(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_autolevel[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -421,7 +421,7 @@ def _bottomhat(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_bottomhat[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -431,7 +431,7 @@ def _equalize(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_equalize[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -441,7 +441,7 @@ def _gradient(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_gradient[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -451,7 +451,7 @@ def _maximum(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_maximum[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -461,7 +461,7 @@ def _mean(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_mean[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -471,7 +471,7 @@ def _subtract_mean(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_subtract_mean[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -481,7 +481,7 @@ def _median(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_median[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -491,7 +491,7 @@ def _minimum(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_minimum[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -501,7 +501,7 @@ def _enhance_contrast(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_enhance_contrast[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -511,7 +511,7 @@ def _modal(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_modal[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -521,7 +521,7 @@ def _pop(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_pop[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -531,7 +531,7 @@ def _sum(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_sum[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -541,7 +541,7 @@ def _threshold(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_threshold[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -551,7 +551,7 @@ def _tophat(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_tophat[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -561,7 +561,7 @@ def _noise_filter(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_noise_filter[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -571,7 +571,7 @@ def _entropy(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_entropy[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -581,7 +581,7 @@ def _otsu(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_otsu[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -591,7 +591,7 @@ def _windowed_hist(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): + signed char shift_x, signed char shift_y, Py_ssize_t max_bin): _core(_kernel_win_hist[dtype_t_out, dtype_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) diff --git a/skimage/filter/rank/percentile_cy.pyx b/skimage/filter/rank/percentile_cy.pyx index 5ff584d3..3be4b52a 100644 --- a/skimage/filter/rank/percentile_cy.pyx +++ b/skimage/filter/rank/percentile_cy.pyx @@ -257,7 +257,7 @@ def _autolevel(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, double p0, double p1, + signed char shift_x, signed char shift_y, double p0, double p1, Py_ssize_t max_bin): _core(_kernel_autolevel[dtype_t_out, dtype_t], image, selem, mask, out, @@ -268,7 +268,7 @@ def _gradient(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, double p0, double p1, + signed char shift_x, signed char shift_y, double p0, double p1, Py_ssize_t max_bin): _core(_kernel_gradient[dtype_t_out, dtype_t], image, selem, mask, out, @@ -279,7 +279,7 @@ def _mean(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, double p0, double p1, + signed char shift_x, signed char shift_y, double p0, double p1, Py_ssize_t max_bin): _core(_kernel_mean[dtype_t_out, dtype_t], image, selem, mask, out, @@ -290,7 +290,7 @@ def _sum(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, double p0, double p1, + signed char shift_x, signed char shift_y, double p0, double p1, Py_ssize_t max_bin): _core(_kernel_sum[dtype_t_out, dtype_t], image, selem, mask, out, @@ -301,7 +301,7 @@ def _subtract_mean(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, double p0, double p1, + signed char shift_x, signed char shift_y, double p0, double p1, Py_ssize_t max_bin): _core(_kernel_subtract_mean[dtype_t_out, dtype_t], image, selem, mask, @@ -312,7 +312,7 @@ def _enhance_contrast(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, double p0, double p1, + signed char shift_x, signed char shift_y, double p0, double p1, Py_ssize_t max_bin): _core(_kernel_enhance_contrast[dtype_t_out, dtype_t], image, selem, mask, @@ -323,7 +323,7 @@ def _percentile(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, double p0, double p1, + signed char shift_x, signed char shift_y, double p0, double p1, Py_ssize_t max_bin): _core(_kernel_percentile[dtype_t_out, dtype_t], image, selem, mask, out, @@ -334,7 +334,7 @@ def _pop(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, double p0, double p1, + signed char shift_x, signed char shift_y, double p0, double p1, Py_ssize_t max_bin): _core(_kernel_pop[dtype_t_out, dtype_t], image, selem, mask, out, @@ -345,7 +345,7 @@ def _threshold(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, :, ::1] out, - char shift_x, char shift_y, double p0, double p1, + signed char shift_x, signed char shift_y, double p0, double p1, Py_ssize_t max_bin): _core(_kernel_threshold[dtype_t_out, dtype_t], image, selem, mask, out, diff --git a/skimage/morphology/cmorph.pyx b/skimage/morphology/cmorph.pyx index 4d491c3b..9457dd9e 100644 --- a/skimage/morphology/cmorph.pyx +++ b/skimage/morphology/cmorph.pyx @@ -11,7 +11,7 @@ from libc.stdlib cimport malloc, free def _dilate(np.uint8_t[:, :] image, np.uint8_t[:, :] selem, np.uint8_t[:, :] out=None, - char shift_x=0, char shift_y=0): + signed char shift_x=0, signed char shift_y=0): """Return greyscale morphological dilation of an image. Morphological dilation sets a pixel at (i,j) to the maximum over all pixels @@ -88,7 +88,7 @@ def _dilate(np.uint8_t[:, :] image, def _erode(np.uint8_t[:, :] image, np.uint8_t[:, :] selem, np.uint8_t[:, :] out=None, - char shift_x=0, char shift_y=0): + signed char shift_x=0, signed char shift_y=0): """Return greyscale morphological erosion of an image. Morphological erosion sets a pixel at (i,j) to the minimum over all pixels From 9214e8193c7dcf7f945e96c13480030a9fc2652c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Fri, 5 Sep 2014 22:24:21 -0400 Subject: [PATCH 0339/1122] DOC: introduce variable num_peaks for reusability --- doc/examples/plot_circular_elliptical_hough_transform.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py index 9b0be0fb..3d24e57c 100755 --- a/doc/examples/plot_circular_elliptical_hough_transform.py +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -60,10 +60,11 @@ radii = [] for radius, h in zip(hough_radii, hough_res): # For each radius, extract two circles - peaks = peak_local_max(h, num_peaks=2) + num_peaks = 2 + peaks = peak_local_max(h, num_peaks=num_peaks) centers.extend(peaks) accums.extend(h[peaks[:, 0], peaks[:, 1]]) - radii.extend([radius, radius]) + radii.extend([radius] * num_peaks) # Draw the most prominent 5 circles image = color.gray2rgb(image) From cfb913d8a865912f521fe3556b96bda7ef565720 Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Tue, 9 Sep 2014 16:16:32 -0400 Subject: [PATCH 0340/1122] add CheckBox widget to viewer. fix ComboBox widget docstring --- skimage/viewer/tests/test_widgets.py | 17 +++++++- skimage/viewer/widgets/core.py | 65 +++++++++++++++++++++++++--- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/skimage/viewer/tests/test_widgets.py b/skimage/viewer/tests/test_widgets.py index ee4c91fb..d83f54ac 100644 --- a/skimage/viewer/tests/test_widgets.py +++ b/skimage/viewer/tests/test_widgets.py @@ -3,7 +3,7 @@ import os from skimage import data, img_as_float, io from skimage.viewer import ImageViewer, viewer_available from skimage.viewer.widgets import ( - Slider, OKCancelButtons, SaveButtons, ComboBox, Text) + Slider, OKCancelButtons, SaveButtons, ComboBox, CheckBox, Text) from skimage.viewer.plugins.base import Plugin from skimage.viewer.qt import QtGui, QtCore from numpy.testing import assert_almost_equal, assert_equal @@ -16,6 +16,20 @@ def get_image_viewer(): viewer += Plugin() return viewer +@skipif(not viewer_available) +def test_check_box(): + viewer = get_image_viewer() + cb = CheckBox('hello', value=True, alignment='left') + viewer.plugins[0] += cb + + assert_equal(cb.val, True) + cb.val = False + assert_equal(cb.val, False) + cb.val = 1 + assert_equal(cb.val, True) + cb.val = 0 + assert_equal(cb.val, False) + @skipif(not viewer_available) def test_combo_box(): @@ -29,7 +43,6 @@ def test_combo_box(): assert_equal(str(cb.val), 'c'), assert_equal(cb.index, 2) - @skipif(not viewer_available) def test_text_widget(): viewer = get_image_viewer() diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 74fe9de4..85302aaa 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -22,7 +22,7 @@ from ..qt.QtCore import Qt from ..utils import RequiredAttr -__all__ = ['BaseWidget', 'Slider', 'ComboBox', 'Text'] +__all__ = ['BaseWidget', 'Slider', 'ComboBox', 'CheckBox', 'Text'] class BaseWidget(QtGui.QWidget): @@ -211,16 +211,16 @@ class ComboBox(BaseWidget): Parameters ---------- name : str - Name of slider parameter. If this parameter is passed as a keyword + Name of ComboBox parameter. If this parameter is passed as a keyword argument, it must match the name of that keyword argument (spaces are replaced with underscores). In addition, this name is displayed as the - name of the slider. - items: list + name of the ComboBox. + items: list of str Allowed parameter values. ptype : {'arg' | 'kwarg' | 'plugin'} Parameter type. callback : function - Callback function called in response to slider changes. This function + Callback function called in response to ComboBox changes. This function is typically set when the widget is added to a plugin. """ @@ -253,3 +253,58 @@ class ComboBox(BaseWidget): @index.setter def index(self, i): self._combo_box.setCurrentIndex(i) + +class CheckBox(BaseWidget): + """CheckBox widget + + Parameters + ---------- + name : str + Name of CheckBox parameter. If this parameter is passed as a keyword + argument, it must match the name of that keyword argument (spaces are + replaced with underscores). In addition, this name is displayed as the + name of the CheckBox. + value: {False, True} + Initial state of the CheckBox. + alignment: {'center','left','right'} + Checkbox alignment + ptype : {'arg' | 'kwarg' | 'plugin'} + Parameter type. + callback : function + Callback function called in response to CheckBox changes. This function + is typically set when the widget is added to a plugin. + """ + + def __init__(self, name, value=False, alignment='center', ptype='kwarg', callback=None): + super(CheckBox, self).__init__(name, ptype, callback) + + self._check_box = QtGui.QCheckBox() + self._check_box.setChecked(value) + self._check_box.setText(self.name) + + self.layout = QtGui.QHBoxLayout(self) + if alignment == 'center': + self.layout.setAlignment(QtCore.Qt.AlignCenter) + elif alignment == 'left': + self.layout.setAlignment(QtCore.Qt.AlignLeft) + elif alignment == 'right': + self.layout.setAlignment(QtCore.Qt.AlignRight) + else: + raise ValueError("Unexpected value %s for 'alignment'" % alignment) + + self.layout.addWidget(self._check_box) + + self._check_box.stateChanged.connect(self._value_changed) + + @property + def val(self): + #return self._check_box.checkState() + return self._check_box.isChecked() + + + @val.setter + def val(self, i): + #self._check_box.setCheckState(i) # setCheckState has 3 states: + # 0=unchecked, 1=partial, 2=checked + self._check_box.setChecked(i) # setChecked has only two states: + # 0 = unchecked, 2 = checked \ No newline at end of file From cb735dca55418200a71bd64bdc1249079474e2dd Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Tue, 9 Sep 2014 19:07:45 -0400 Subject: [PATCH 0341/1122] added tv_denoise.py viewer example using the CheckBox --- viewer_examples/plugins/tv_denoise.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 viewer_examples/plugins/tv_denoise.py diff --git a/viewer_examples/plugins/tv_denoise.py b/viewer_examples/plugins/tv_denoise.py new file mode 100644 index 00000000..05b57205 --- /dev/null +++ b/viewer_examples/plugins/tv_denoise.py @@ -0,0 +1,25 @@ +from skimage import data +from skimage.restoration import denoise_tv_chambolle +from numpy import random, clip, uint8 + +from skimage.viewer import ImageViewer +from skimage.viewer.widgets import Slider, CheckBox, OKCancelButtons, SaveButtons +from skimage.viewer.plugins.base import Plugin + + +image = data.chelsea() +sigma = 30 + +image = image + random.normal(loc=0, scale=sigma, size=image.shape) +image = clip(image,0,255).astype(uint8) +viewer = ImageViewer(image) + +plugin = Plugin(image_filter=denoise_tv_chambolle) +plugin += Slider('weight', 0, 5, value=0.3, value_type='float') +plugin += Slider('n_iter_max', 1, 1000, value=100, value_type='int') +plugin += CheckBox('multichannel',value=True) +plugin += SaveButtons() +plugin += OKCancelButtons() + +viewer += plugin +viewer.show() From 026fc8eef1cfc5b9bf1a07a2b63691410eb8b662 Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Wed, 10 Sep 2014 14:09:11 -0400 Subject: [PATCH 0342/1122] fix image scaling in tv_denoise.py example --- viewer_examples/plugins/tv_denoise.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/viewer_examples/plugins/tv_denoise.py b/viewer_examples/plugins/tv_denoise.py index 05b57205..370efea3 100644 --- a/viewer_examples/plugins/tv_denoise.py +++ b/viewer_examples/plugins/tv_denoise.py @@ -1,22 +1,23 @@ from skimage import data from skimage.restoration import denoise_tv_chambolle -from numpy import random, clip, uint8 +from skimage.util import img_as_float +from numpy import random, clip from skimage.viewer import ImageViewer from skimage.viewer.widgets import Slider, CheckBox, OKCancelButtons, SaveButtons from skimage.viewer.plugins.base import Plugin -image = data.chelsea() -sigma = 30 +image = img_as_float(data.chelsea()) +sigma = 30/255. image = image + random.normal(loc=0, scale=sigma, size=image.shape) -image = clip(image,0,255).astype(uint8) +image = clip(image,0,1) viewer = ImageViewer(image) plugin = Plugin(image_filter=denoise_tv_chambolle) -plugin += Slider('weight', 0, 5, value=0.3, value_type='float') -plugin += Slider('n_iter_max', 1, 1000, value=100, value_type='int') +plugin += Slider('weight', 0.01, 5, value=0.3, value_type='float') +plugin += Slider('n_iter_max', 1, 100, value=20, value_type='int') plugin += CheckBox('multichannel',value=True) plugin += SaveButtons() plugin += OKCancelButtons() From 28a41eeb2b50ab2bc9f78d50d8f67480be05b09a Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 10 Sep 2014 19:21:21 -0500 Subject: [PATCH 0343/1122] Clear the blit background when the image changes --- skimage/viewer/viewers/core.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 00902112..36ba6ee6 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -77,6 +77,8 @@ class BlitManager(object): self.canvas.restore_region(self.background) self.draw_artists() self.canvas.blit(self.ax.bbox) + else: + self.canvas.draw_idle() def draw_artists(self): for artist in self.artists: @@ -368,6 +370,9 @@ class ImageViewer(QtGui.QMainWindow): clim = (0, clim[1]) self._image_plot.set_clim(clim) + if self.useblit: + self._blit_manager.background = None + self.redraw() def reset_image(self): From f078bcf779d205cdf12e44fcacf9c510a978d091 Mon Sep 17 00:00:00 2001 From: capitanbatata Date: Thu, 11 Sep 2014 11:09:27 +0200 Subject: [PATCH 0344/1122] Removed the print statements from the test code. --- skimage/__init__.py | 1 + skimage/color/tests/test_colorconv.py | 15 +++++---------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/skimage/__init__.py b/skimage/__init__.py index 59c80ed2..8b5d4056 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -72,6 +72,7 @@ except ImportError: del version + try: _imp.find_module('nose') except ImportError: diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index cd591829..e9ac83ee 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -236,13 +236,11 @@ class TestColorconv(TestCase): ## Test the conversion with the rest of the illuminants. for I in ["d50", "d55", "d65", "d75"]: for obs in ["2", "10"]: - print("testing illuminant={0}, observer={1}".format(I, obs)) fname = "lab_array_{0}_{1}.npy".format(I, obs) lab_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(lab_array_I_obs, xyz2lab(self.xyz_array, I, obs), decimal=2) for I in ["a", "e"]: - print("testing illuminant={0}, observer=2".format(I)) fname = "lab_array_{0}_2.npy".format(I) lab_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(lab_array_I_obs, @@ -267,15 +265,14 @@ class TestColorconv(TestCase): ## And we include a call to test the exception handling in the code. try: - xs = lab2xyz(lab_array_I_obs, "NaI", "2") # Not an illuminant + xs = lab2xyz(lab_array_I_obs, "NaI", "2") # Not an illuminant except ValueError: - print 'Correctly handled the unknown illuminant case.' - + pass + try: - xs = lab2xyz(lab_array_I_obs, "d50", "42") # Not an illuminant + xs = lab2xyz(lab_array_I_obs, "d50", "42") # Not an observer degree except ValueError: - print 'Correctly handled the unknown observer case.' - + pass def test_rgb2lab_brucelindbloom(self): """ @@ -310,13 +307,11 @@ class TestColorconv(TestCase): ## Test the conversion with the rest of the illuminants. for I in ["d50", "d55", "d65", "d75"]: for obs in ["2", "10"]: - print("testing illuminant={0}, observer={1}".format(I, obs)) fname = "luv_array_{0}_{1}.npy".format(I, obs) luv_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(luv_array_I_obs, xyz2luv(self.xyz_array, I, obs), decimal=2) for I in ["a", "e"]: - print("testing illuminant={0}, observer=2".format(I)) fname = "luv_array_{0}_2.npy".format(I) luv_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(luv_array_I_obs, From 27fcb81f74d42de0bc6761a632aaca2897cf8d16 Mon Sep 17 00:00:00 2001 From: capitanbatata Date: Thu, 11 Sep 2014 18:23:55 +0200 Subject: [PATCH 0345/1122] Incorporated Stefan's comments. --- skimage/__init__.py | 1 - skimage/color/colorconv.py | 5 +-- skimage/color/tests/test_colorconv.py | 60 ++++++++++++++++----------- 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/skimage/__init__.py b/skimage/__init__.py index 8b5d4056..59c80ed2 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -72,7 +72,6 @@ except ImportError: del version - try: _imp.find_module('nose') except ImportError: diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 53642b1f..e8a95db8 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -56,7 +56,6 @@ from __future__ import division import numpy as np from scipy import linalg from ..util import dtype -from skimage._shared.utils import deprecated # Imported but unused. Remove? def guess_spatial_dimensions(image): @@ -396,7 +395,7 @@ def get_xyz_coords(illuminant, observer): return illuminants[illuminant][observer] except KeyError: raise ValueError("Unknown illuminant/observer combination\ - (\"{0}\", \"{1}\")".format(illuminant, observer)) + (\'{0}\', \'{1}\')".format(illuminant, observer)) # Haematoxylin-Eosin-DAB colorspace # From original Ruifrok's paper: A. C. Ruifrok and D. A. Johnston, @@ -765,7 +764,7 @@ def xyz2lab(xyz, illuminant="D65", observer="2"): ValueError If `xyz` is not a 3-D array of shape ``(.., ..,[ ..,] 3)``. ValueError - If either the illuminant or the observer angle are not supported or + If either the illuminant or the observer angle is unsupported or unknown. Notes diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index e9ac83ee..cf960f4e 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -227,50 +227,57 @@ class TestColorconv(TestCase): def test_rgb2grey_on_grey(self): rgb2grey(np.random.rand(5, 5)) - # test matrices for xyz2lab and lab2xyz generated using http://www.easyrgb.com/index.php?X=CALC + # test matrices for xyz2lab and lab2xyz generated using + # http://www.easyrgb.com/index.php?X=CALC # Note: easyrgb website displays xyz*100 def test_xyz2lab(self): assert_array_almost_equal(xyz2lab(self.xyz_array), self.lab_array, decimal=3) - ## Test the conversion with the rest of the illuminants. + # Test the conversion with the rest of the illuminants. for I in ["d50", "d55", "d65", "d75"]: for obs in ["2", "10"]: fname = "lab_array_{0}_{1}.npy".format(I, obs) - lab_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) + lab_array_I_obs = np.load( + os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(lab_array_I_obs, - xyz2lab(self.xyz_array, I, obs), decimal=2) + xyz2lab(self.xyz_array, I, obs), + decimal=2) for I in ["a", "e"]: fname = "lab_array_{0}_2.npy".format(I) - lab_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) + lab_array_I_obs = np.load( + os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(lab_array_I_obs, - xyz2lab(self.xyz_array, I, "2"), decimal=2) + xyz2lab(self.xyz_array, I, "2"), + decimal=2) def test_lab2xyz(self): assert_array_almost_equal(lab2xyz(self.lab_array), self.xyz_array, decimal=3) - ## Test the conversion with the rest of the illuminants. + # Test the conversion with the rest of the illuminants. for I in ["d50", "d55", "d65", "d75"]: for obs in ["2", "10"]: fname = "lab_array_{0}_{1}.npy".format(I, obs) - lab_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) - assert_array_almost_equal(lab2xyz(lab_array_I_obs, I, obs), + lab_array_I_obs = np.load( + os.path.join(os.path.dirname(__file__), 'data', fname)) + assert_array_almost_equal(lab2xyz(lab_array_I_obs, I, obs), self.xyz_array, decimal=3) for I in ["a", "e"]: fname = "lab_array_{0}_2.npy".format(I, obs) - lab_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) + lab_array_I_obs = np.load( + os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(lab2xyz(lab_array_I_obs, I, "2"), - self.xyz_array, decimal=3) + self.xyz_array, decimal=3) - ## And we include a call to test the exception handling in the code. + # And we include a call to test the exception handling in the code. try: xs = lab2xyz(lab_array_I_obs, "NaI", "2") # Not an illuminant except ValueError: pass - + try: - xs = lab2xyz(lab_array_I_obs, "d50", "42") # Not an observer degree + xs = lab2xyz(lab_array_I_obs, "d50", "42") # Not a degree except ValueError: pass @@ -304,36 +311,41 @@ class TestColorconv(TestCase): assert_array_almost_equal(xyz2luv(self.xyz_array), self.luv_array, decimal=3) - ## Test the conversion with the rest of the illuminants. + # Test the conversion with the rest of the illuminants. for I in ["d50", "d55", "d65", "d75"]: for obs in ["2", "10"]: fname = "luv_array_{0}_{1}.npy".format(I, obs) - luv_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) + luv_array_I_obs = np.load( + os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(luv_array_I_obs, - xyz2luv(self.xyz_array, I, obs), decimal=2) + xyz2luv(self.xyz_array, I, obs), + decimal=2) for I in ["a", "e"]: fname = "luv_array_{0}_2.npy".format(I) - luv_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) + luv_array_I_obs = np.load( + os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(luv_array_I_obs, - xyz2luv(self.xyz_array, I, "2"), decimal=2) - + xyz2luv(self.xyz_array, I, "2"), + decimal=2) def test_luv2xyz(self): assert_array_almost_equal(luv2xyz(self.luv_array), self.xyz_array, decimal=3) - ## Test the conversion with the rest of the illuminants. + # Test the conversion with the rest of the illuminants. for I in ["d50", "d55", "d65", "d75"]: for obs in ["2", "10"]: fname = "luv_array_{0}_{1}.npy".format(I, obs) - luv_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) + luv_array_I_obs = np.load( + os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(luv2xyz(luv_array_I_obs, I, obs), self.xyz_array, decimal=3) for I in ["a", "e"]: fname = "luv_array_{0}_2.npy".format(I, obs) - luv_array_I_obs = np.load(os.path.join(os.path.dirname(__file__), 'data', fname)) + luv_array_I_obs = np.load( + os.path.join(os.path.dirname(__file__), 'data', fname)) assert_array_almost_equal(luv2xyz(luv_array_I_obs, I, "2"), - self.xyz_array, decimal=3) + self.xyz_array, decimal=3) def test_rgb2luv_brucelindbloom(self): """ From c87d5567b4a6bcc324fa66ae1de594f8f0e1e62e Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 12 Sep 2014 14:07:46 +1000 Subject: [PATCH 0346/1122] Update find_contours docstring to use r/c notation This update fixes #1140. Other docstrings will probably need to be updated as well. --- skimage/measure/_find_contours.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/measure/_find_contours.py b/skimage/measure/_find_contours.py index 0eea9126..ff53795f 100755 --- a/skimage/measure/_find_contours.py +++ b/skimage/measure/_find_contours.py @@ -35,7 +35,7 @@ def find_contours(array, level, ------- contours : list of (n,2)-ndarrays Each contour is an ndarray of shape ``(n, 2)``, - consisting of n ``(x, y)`` coordinates along the contour. + consisting of n ``(row, column)`` coordinates along the contour. Notes ----- From 82578234b570ba741ccdfb8c09612b18a9ed7d92 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 12 Sep 2014 06:14:45 -0500 Subject: [PATCH 0347/1122] Fix swirl docstring to use (row, column) nomenclature --- skimage/transform/_warps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 23b56164..720b3a24 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -294,7 +294,7 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0, ---------- image : ndarray Input image. - center : (x,y) tuple or (2,) ndarray, optional + center : (row, column) tuple or (2,) ndarray, optional Center coordinate of transformation. strength : float, optional The amount of swirling applied. From 02a6e210ea9858ebc4c74a008324a2a775dc9d43 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 12 Sep 2014 06:15:28 -0500 Subject: [PATCH 0348/1122] Remove extra space --- skimage/transform/_warps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 720b3a24..27c92ff7 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -294,7 +294,7 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0, ---------- image : ndarray Input image. - center : (row, column) tuple or (2,) ndarray, optional + center : (row, column) tuple or (2,) ndarray, optional Center coordinate of transformation. strength : float, optional The amount of swirling applied. From 3f1b8a44685e118cadc5aa60ba01b137d81cb735 Mon Sep 17 00:00:00 2001 From: Gregory Lee Date: Fri, 12 Sep 2014 19:37:09 -0700 Subject: [PATCH 0349/1122] code formatting fixes --- skimage/viewer/widgets/core.py | 17 ++++++++--------- viewer_examples/plugins/tv_denoise.py | 7 ++++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 85302aaa..9a99d7e8 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -254,6 +254,7 @@ class ComboBox(BaseWidget): def index(self, i): self._combo_box.setCurrentIndex(i) + class CheckBox(BaseWidget): """CheckBox widget @@ -265,17 +266,18 @@ class CheckBox(BaseWidget): replaced with underscores). In addition, this name is displayed as the name of the CheckBox. value: {False, True} - Initial state of the CheckBox. + Initial state of the CheckBox. alignment: {'center','left','right'} - Checkbox alignment + Checkbox alignment ptype : {'arg' | 'kwarg' | 'plugin'} - Parameter type. + Parameter type callback : function Callback function called in response to CheckBox changes. This function is typically set when the widget is added to a plugin. """ - def __init__(self, name, value=False, alignment='center', ptype='kwarg', callback=None): + def __init__(self, name, value=False, alignment='center', ptype='kwarg', + callback=None): super(CheckBox, self).__init__(name, ptype, callback) self._check_box = QtGui.QCheckBox() @@ -298,13 +300,10 @@ class CheckBox(BaseWidget): @property def val(self): - #return self._check_box.checkState() return self._check_box.isChecked() - @val.setter def val(self, i): - #self._check_box.setCheckState(i) # setCheckState has 3 states: - # 0=unchecked, 1=partial, 2=checked self._check_box.setChecked(i) # setChecked has only two states: - # 0 = unchecked, 2 = checked \ No newline at end of file + # 0 = unchecked, 2 = checked + \ No newline at end of file diff --git a/viewer_examples/plugins/tv_denoise.py b/viewer_examples/plugins/tv_denoise.py index 370efea3..e4d06fb2 100644 --- a/viewer_examples/plugins/tv_denoise.py +++ b/viewer_examples/plugins/tv_denoise.py @@ -4,7 +4,8 @@ from skimage.util import img_as_float from numpy import random, clip from skimage.viewer import ImageViewer -from skimage.viewer.widgets import Slider, CheckBox, OKCancelButtons, SaveButtons +from skimage.viewer.widgets import (Slider, CheckBox, OKCancelButtons, + SaveButtons) from skimage.viewer.plugins.base import Plugin @@ -12,13 +13,13 @@ image = img_as_float(data.chelsea()) sigma = 30/255. image = image + random.normal(loc=0, scale=sigma, size=image.shape) -image = clip(image,0,1) +image = clip(image, 0, 1) viewer = ImageViewer(image) plugin = Plugin(image_filter=denoise_tv_chambolle) plugin += Slider('weight', 0.01, 5, value=0.3, value_type='float') plugin += Slider('n_iter_max', 1, 100, value=20, value_type='int') -plugin += CheckBox('multichannel',value=True) +plugin += CheckBox('multichannel', value=True) plugin += SaveButtons() plugin += OKCancelButtons() From 5877e2ee3d6cc879b6fe045452011885198d0557 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 13 Sep 2014 07:42:52 -0500 Subject: [PATCH 0350/1122] Remove blit background when overlay changes --- skimage/viewer/plugins/overlayplugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index 7f016328..63ff61a3 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -65,6 +65,9 @@ class OverlayPlugin(Plugin): else: update_axes_image(self._overlay_plot, image) + if self.image_viewer.useblit: + self.image_viewer._blit_manager.background = None + self.image_viewer.redraw() @property From 1a1d665c0aea3a101e6b79836415a90a1179dd6d Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 13 Sep 2014 08:10:14 -0500 Subject: [PATCH 0351/1122] Use functools.wraps in default_fallback dectorator --- skimage/morphology/misc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index edf2892b..fe1f27d8 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -1,4 +1,5 @@ import numpy as np +import functools import scipy.ndimage as nd from .selem import _default_selem @@ -31,7 +32,7 @@ def default_fallback(func): If the image dimentionality is greater than 2D, the ndimage function is returned, otherwise skimage function is used. """ - + @functools.wraps(func) def func_out(image, selem=None, out=None, **kwargs): # Default structure element if selem is None: From c85e08b0edabe3bb4650a4643472187291936038 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 15 Sep 2014 05:58:00 -0500 Subject: [PATCH 0352/1122] Rename (x, y) -> (row, col) in _geometric.py. --- skimage/transform/_geometric.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index b3019135..bd93e959 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -919,7 +919,7 @@ def warp_coords(coord_map, shape, dtype=np.float64): coord_map : callable like GeometricTransform.inverse Return input coordinates for given output coordinates. Coordinates are in the shape (P, 2), where P is the number - of coordinates and each element is a ``(x, y)`` pair. + of coordinates and each element is a ``(row, col)`` pair. shape : tuple Shape of output image ``(rows, cols[, bands])``. dtype : np.dtype or string @@ -966,10 +966,10 @@ def warp_coords(coord_map, shape, dtype=np.float64): coords_shape.append(shape[2]) coords = np.empty(coords_shape, dtype=dtype) - # Reshape grid coordinates into a (P, 2) array of (x, y) pairs + # Reshape grid coordinates into a (P, 2) array of (row, col) pairs tf_coords = np.indices((cols, rows), dtype=dtype).reshape(2, -1).T - # Map each (x, y) pair to the source image according to + # Map each (row, col) pair to the source image according to # the user-provided mapping tf_coords = coord_map(tf_coords) @@ -998,7 +998,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, Input image. inverse_map : transformation object, callable ``xy = f(xy, **kwargs)``, (3, 3) array Inverse coordinate map. A function that transforms a (N, 2) array of - ``(x, y)`` coordinates in the *output image* into their corresponding + ``(row, col)`` coordinates in the *output image* into their corresponding coordinates in the *source image* (e.g. a transformation object or its inverse). See example section for usage. map_args : dict, optional From 15ffd52299360e0d9de89cbc50a8f70178fcbc12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 10 Sep 2014 13:42:57 -0400 Subject: [PATCH 0353/1122] Improve desciption of inverse_map and add option to direclty pass coordinates --- skimage/transform/_geometric.py | 50 ++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index bd93e959..fd34d815 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -996,11 +996,26 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, ---------- image : 2-D or 3-D array Input image. - inverse_map : transformation object, callable ``xy = f(xy, **kwargs)``, (3, 3) array - Inverse coordinate map. A function that transforms a (N, 2) array of - ``(row, col)`` coordinates in the *output image* into their corresponding - coordinates in the *source image* (e.g. a transformation object or its - inverse). See example section for usage. + inverse_map : transformation object, callable ``xy = f(xy, **kwargs)``, ndarray + Inverse coordinate map, which transforms coordinates in the *output + images* into their corresponding coordinates in the *source image*. + + There are a number of different options to define this map: + + - For 2-D images, you can directly pass a transformation object, + e.g. `skimage.transform.SimilarityTransform`, or its inverse. + - For 2-D images, you can pass a (3, 3) homogeneous transformation + matrix, e.g. `skimage.transform.SimilarityTransform.params` + - For M-D images, a function that transforms a (N, M) coordinates. + In case of 2-D images this means a function that transforms a + (N, 2) array of ``(x, y)`` coordinates in the *output image* into + their corresponding coordinates in the *source image*. Extra + parameters to the function can be specified through `map_args`. + - For M-D images, you can directly pass an array of coordinates. + See `scipy.ndimage.map_coordinates`. Note, that a (3, 3) matrix + is interpreted as a homogeneous transformation matrix. + + See example section for usage. map_args : dict, optional Keyword arguments passed to `inverse_map`. output_shape : tuple (rows, cols), optional @@ -1009,12 +1024,12 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, and columns need to be specified. order : int, optional The order of interpolation. The order has to be in the range 0-5: - * 0: Nearest-neighbor - * 1: Bi-linear (default) - * 2: Bi-quadratic - * 3: Bi-cubic - * 4: Bi-quartic - * 5: Bi-quintic + - 0: Nearest-neighbor + - 1: Bi-linear (default) + - 2: Bi-quadratic + - 3: Bi-cubic + - 4: Bi-quartic + - 5: Bi-quintic mode : string, optional Points outside the boundaries of the input are filled according to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). @@ -1120,14 +1135,17 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, if out is None: # use ndimage.map_coordinates rows, cols = output_shape[:2] - # inverse_map is a transformation matrix as numpy array + # inverse_map is a transformation matrix as numpy array, this is only + # used for order >= 4. if isinstance(inverse_map, np.ndarray) and inverse_map.shape == (3, 3): inverse_map = ProjectiveTransform(matrix=inverse_map) - def coord_map(*args): - return inverse_map(*args, **map_args) - - coords = warp_coords(coord_map, (rows, cols, bands)) + if isinstance(inverse_map, np.ndarray): + coords = inverse_map + else: + def coord_map(*args): + return inverse_map(*args, **map_args) + coords = warp_coords(coord_map, (rows, cols, bands)) # Pre-filtering not necessary for order 0, 1 interpolation prefilter = order > 1 From ca9a155cf20738d606c0dba99c1f18f83bb5581f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 11 Sep 2014 10:28:58 -0400 Subject: [PATCH 0354/1122] Improve inverse_map description --- skimage/transform/_geometric.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index fd34d815..5e0e7bc0 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -1006,14 +1006,23 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, e.g. `skimage.transform.SimilarityTransform`, or its inverse. - For 2-D images, you can pass a (3, 3) homogeneous transformation matrix, e.g. `skimage.transform.SimilarityTransform.params` - - For M-D images, a function that transforms a (N, M) coordinates. - In case of 2-D images this means a function that transforms a - (N, 2) array of ``(x, y)`` coordinates in the *output image* into - their corresponding coordinates in the *source image*. Extra + - For M-D images, a function that transforms a (N, M) coordinate + matrix in the output image to their corresponding coordinates in + the source image, where N is the total number of pixels in the + output image. In case of 2-D images this means a function that + transforms a (N, 2) array of ``(x, y)`` coordinates. Extra parameters to the function can be specified through `map_args`. - For M-D images, you can directly pass an array of coordinates. - See `scipy.ndimage.map_coordinates`. Note, that a (3, 3) matrix - is interpreted as a homogeneous transformation matrix. + The first dimension specifies the coordinates in the source image, + while the subsequent dimensions determine the position in the + output image. In case of 2-D images, you need to pass an array of + shape ``(2, rows, cols)``, where `rows` and `cols` determine the + shape of the output image, and the first dimension contains the + ``(row, col)`` coordinate in the source image. Note, that a + ``(3, 3)`` matrix is interpreted as a homogeneous transformation + matrix, so you cannot interpolate values from a 3-D input, if the + output is of shape ``(3, )``. See `scipy.ndimage.map_coordinates` + for further documentation. See example section for usage. map_args : dict, optional From 1a31968b5453750ec7447dc0d3056111cb0adc36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 11 Sep 2014 11:25:22 -0400 Subject: [PATCH 0355/1122] Make warp function N-D compatible, and add example to doc string --- skimage/transform/_geometric.py | 111 +++++++++++++++++++------------- 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 5e0e7bc0..647adafe 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -994,35 +994,37 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, Parameters ---------- - image : 2-D or 3-D array + image : ndarray Input image. inverse_map : transformation object, callable ``xy = f(xy, **kwargs)``, ndarray - Inverse coordinate map, which transforms coordinates in the *output - images* into their corresponding coordinates in the *source image*. + Inverse coordinate map, which transforms coordinates in the output + images into their corresponding coordinates in the input image. - There are a number of different options to define this map: + There are a number of different options to define this map, depending + on the dimensionality of the input image. A 2-D image can have 2 + dimensions for gray-scale images, or 3 dimensions with color + information. - For 2-D images, you can directly pass a transformation object, e.g. `skimage.transform.SimilarityTransform`, or its inverse. - For 2-D images, you can pass a (3, 3) homogeneous transformation matrix, e.g. `skimage.transform.SimilarityTransform.params` - - For M-D images, a function that transforms a (N, M) coordinate - matrix in the output image to their corresponding coordinates in - the source image, where N is the total number of pixels in the - output image. In case of 2-D images this means a function that - transforms a (N, 2) array of ``(x, y)`` coordinates. Extra - parameters to the function can be specified through `map_args`. - - For M-D images, you can directly pass an array of coordinates. - The first dimension specifies the coordinates in the source image, + - For 2-D images, a function that transforms a ``(M, 2)`` array of + ``(x, y)`` coordinates in the output image to their corresponding + coordinates in the input image. Extra parameters to the function + can be specified through `map_args`. + - For N-D images, you can directly pass an array of coordinates. + The first dimension specifies the coordinates in the input image, while the subsequent dimensions determine the position in the - output image. In case of 2-D images, you need to pass an array of - shape ``(2, rows, cols)``, where `rows` and `cols` determine the + output image. E.g. in case of 2-D images, you need to pass an array + of shape ``(2, rows, cols)``, where `rows` and `cols` determine the shape of the output image, and the first dimension contains the - ``(row, col)`` coordinate in the source image. Note, that a - ``(3, 3)`` matrix is interpreted as a homogeneous transformation - matrix, so you cannot interpolate values from a 3-D input, if the - output is of shape ``(3, )``. See `scipy.ndimage.map_coordinates` - for further documentation. + ``(row, col)`` coordinate in the input image. + See `scipy.ndimage.map_coordinates` for further documentation. + + Note, that a ``(3, 3)`` matrix is interpreted as a homogeneous + transformation matrix, so you cannot interpolate values from a 3-D + input, if the output is of shape ``(3, )``. See example section for usage. map_args : dict, optional @@ -1086,6 +1088,28 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, >>> warped = warp(image, tform.inverse) + For N-D images you can pass a coordinate array, that specifies the + coordinates in the input image for every element in the output image. E.g. + if you want to rescale a 3-D cube, you can do: + + >>> cube_shape = np.array([30, 30, 30]) + >>> cube = np.random.rand(*cube_shape) + + Setup the coordinate array, that defines the scaling: + + >>> scale = 0.1 + >>> output_shape = (scale * cube_shape).astype(int) + >>> coords0, coords1, coords2 = \ + ... np.mgrid[:output_shape[0], :output_shape[1], :output_shape[2]] + >>> coords = np.array([coords0, coords1, coords2]) + + Assume that the cube contains spatial data, where the first array element + center is at coordinate (0.5, 0.5, 0.5) in real space, i.e. we have to + account for this extra offset when scaling the image: + + >>> coords = (coords + 0.5) / scale - 0.5 + >>> warped = warp(cube, coords) + """ # Backward API compatibility if reverse_map is not None: @@ -1093,20 +1117,14 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, 'the `inverse_map` parameter.') inverse_map = reverse_map - if image.ndim < 2 or image.ndim > 3: - raise ValueError("Input must have 2 or 3 dimensions.") - - orig_ndim = image.ndim - image = np.atleast_3d(img_as_float(image)) - ishape = np.array(image.shape) - bands = ishape[2] + image = img_as_float(image) + input_shape = np.array(image.shape) if output_shape is None: - output_shape = ishape + output_shape = input_shape else: output_shape = safe_as_int(output_shape) - out = None # use fast Cython version for specific interpolation orders and input @@ -1131,30 +1149,35 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, if matrix is not None: matrix = matrix.astype(np.double) - # transform all bands - dims = [] - for dim in range(image.shape[2]): - dims.append(_warp_fast(image[..., dim], matrix, - output_shape=output_shape, - order=order, mode=mode, cval=cval)) - out = np.dstack(dims) - if orig_ndim == 2: - out = out[..., 0] + if image.ndim == 2: + out = _warp_fast(image, matrix, + output_shape=output_shape, + order=order, mode=mode, cval=cval) + elif image.ndim == 3: + dims = [] + for dim in range(image.shape[2]): + dims.append(_warp_fast(image[..., dim], matrix, + output_shape=output_shape, + order=order, mode=mode, cval=cval)) + out = np.dstack(dims) if out is None: # use ndimage.map_coordinates - rows, cols = output_shape[:2] - # inverse_map is a transformation matrix as numpy array, this is only # used for order >= 4. - if isinstance(inverse_map, np.ndarray) and inverse_map.shape == (3, 3): + if (isinstance(inverse_map, np.ndarray) + and inverse_map.shape == (3, 3)): inverse_map = ProjectiveTransform(matrix=inverse_map) if isinstance(inverse_map, np.ndarray): coords = inverse_map else: + if image.ndim < 2 or image.ndim > 3: + raise ValueError("Input must have 2 or 3 dimensions.") + def coord_map(*args): return inverse_map(*args, **map_args) - coords = warp_coords(coord_map, (rows, cols, bands)) + + coords = warp_coords(coord_map, output_shape) # Pre-filtering not necessary for order 0, 1 interpolation prefilter = order > 1 @@ -1170,8 +1193,4 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, out = clipped - if out.ndim == 3 and orig_ndim == 2: - # remove singleton dimension introduced by atleast_3d - return out[..., 0] - else: - return out + return out From 9b5b5b8cc5bc06ece3d5e8ffc81238e49f34b128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 11 Sep 2014 11:26:07 -0400 Subject: [PATCH 0356/1122] Remove deprecated parameter of warp --- skimage/transform/_geometric.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 647adafe..1c397a1e 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -1111,11 +1111,6 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, >>> warped = warp(cube, coords) """ - # Backward API compatibility - if reverse_map is not None: - warnings.warn('`reverse_map` parameter is deprecated and replaced by ' - 'the `inverse_map` parameter.') - inverse_map = reverse_map image = img_as_float(image) input_shape = np.array(image.shape) From 27d765abd5f54fe700f5d27acb08d8b6f6b3ce2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 11 Sep 2014 11:28:06 -0400 Subject: [PATCH 0357/1122] Highlight fact that warp_coords is only meant for 2(+1)-D images --- skimage/transform/_geometric.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 1c397a1e..cf099c74 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -912,7 +912,7 @@ def _stackcopy(a, b): def warp_coords(coord_map, shape, dtype=np.float64): - """Build the source coordinates for the output pixels of an image warp. + """Build the source coordinates for the output of a 2-D image warp. Parameters ---------- @@ -934,8 +934,9 @@ def warp_coords(coord_map, shape, dtype=np.float64): Notes ----- - This is a lower-level routine that produces the source coordinates used by - `warp()`. + + This is a lower-level routine that produces the source coordinates for 2-D + images used by `warp()`. It is provided separately from `warp` to give additional flexibility to users who would like, for example, to re-use a particular coordinate @@ -946,7 +947,7 @@ def warp_coords(coord_map, shape, dtype=np.float64): Examples -------- - Produce a coordinate map that Shifts an image up and to the right: + Produce a coordinate map that shifts an image up and to the right: >>> from skimage import data >>> from scipy.ndimage import map_coordinates From 1b49b494ce30f973ca9d420d62debe267f0a7625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 12 Sep 2014 16:51:16 -0400 Subject: [PATCH 0358/1122] Make error message for wrong input dim more explicit --- skimage/transform/_geometric.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index cf099c74..8af5588c 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -1168,7 +1168,9 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, coords = inverse_map else: if image.ndim < 2 or image.ndim > 3: - raise ValueError("Input must have 2 or 3 dimensions.") + raise ValueError("Only 2-D images (grayscale or color) are " + "supported, when providing a callable " + "`inverse_map`.") def coord_map(*args): return inverse_map(*args, **map_args) From f9009410ba1333ff432bd273de5637bab4c6e6d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 15 Sep 2014 23:27:03 -0400 Subject: [PATCH 0359/1122] Add comments to different conditional paths --- skimage/transform/_geometric.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 8af5588c..bba323b8 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -1158,15 +1158,20 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, out = np.dstack(dims) if out is None: # use ndimage.map_coordinates - # inverse_map is a transformation matrix as numpy array, this is only - # used for order >= 4. + # inverse_map is a transformation matrix as numpy array, + # this is only used for order >= 4. if (isinstance(inverse_map, np.ndarray) and inverse_map.shape == (3, 3)): inverse_map = ProjectiveTransform(matrix=inverse_map) if isinstance(inverse_map, np.ndarray): + # inverse_map is directly given as coordinates coords = inverse_map else: + # inverse_map is given as function, that transforms (N, 2) + # destination coordinates to their corresponding source + # coordinates. This is only supported for 2(+1)-D images. + if image.ndim < 2 or image.ndim > 3: raise ValueError("Only 2-D images (grayscale or color) are " "supported, when providing a callable " @@ -1179,6 +1184,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, # Pre-filtering not necessary for order 0, 1 interpolation prefilter = order > 1 + out = ndimage.map_coordinates(image, coords, prefilter=prefilter, mode=mode, order=order, cval=cval) From 9b4f2d903202f76e70b1d1ff4e790355cc1e3836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 15 Sep 2014 23:34:33 -0400 Subject: [PATCH 0360/1122] Add support for 2(+1)D images and given output_shape for 2D images --- skimage/transform/_geometric.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index bba323b8..4a1ce6a4 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -1180,6 +1180,13 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, def coord_map(*args): return inverse_map(*args, **map_args) + # Input image is 2D and has color channel, but output_shape is + # given for 2-D images. Automatically add the color channel + # dimensionality. + if len(input_shape) == 3 and len(output_shape) == 2: + output_shape = (output_shape[0], output_shape[1], + input_shape[2]) + coords = warp_coords(coord_map, output_shape) # Pre-filtering not necessary for order 0, 1 interpolation From da36750d55e4c8949fec0a64ff705d2c0183ab8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 16 Sep 2014 08:19:48 -0400 Subject: [PATCH 0361/1122] Make row, col ordering clearer in doc string --- skimage/transform/_geometric.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 4a1ce6a4..20d104e6 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -997,7 +997,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, ---------- image : ndarray Input image. - inverse_map : transformation object, callable ``xy = f(xy, **kwargs)``, ndarray + inverse_map : transformation object, callable ``cr = f(cr, **kwargs)``, or ndarray Inverse coordinate map, which transforms coordinates in the output images into their corresponding coordinates in the input image. @@ -1008,12 +1008,13 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, - For 2-D images, you can directly pass a transformation object, e.g. `skimage.transform.SimilarityTransform`, or its inverse. - - For 2-D images, you can pass a (3, 3) homogeneous transformation - matrix, e.g. `skimage.transform.SimilarityTransform.params` + - For 2-D images, you can pass a ``(3, 3)`` homogeneous + transformation matrix, e.g. + `skimage.transform.SimilarityTransform.params` - For 2-D images, a function that transforms a ``(M, 2)`` array of - ``(x, y)`` coordinates in the output image to their corresponding - coordinates in the input image. Extra parameters to the function - can be specified through `map_args`. + ``(col, row)`` coordinates in the output image to their + corresponding coordinates in the input image. Extra parameters to + the function can be specified through `map_args`. - For N-D images, you can directly pass an array of coordinates. The first dimension specifies the coordinates in the input image, while the subsequent dimensions determine the position in the @@ -1025,7 +1026,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, Note, that a ``(3, 3)`` matrix is interpreted as a homogeneous transformation matrix, so you cannot interpolate values from a 3-D - input, if the output is of shape ``(3, )``. + input, if the output is of shape ``(3,)``. See example section for usage. map_args : dict, optional From 0eeebea22c8d7b46bd615d86d3fd061565f3534f Mon Sep 17 00:00:00 2001 From: SemiColon Warrier Date: Tue, 16 Sep 2014 16:31:39 +0200 Subject: [PATCH 0362/1122] updated local_binary_pattern documentation The reference link was divided into two lines thereby linking to only half the url --- skimage/feature/texture.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py index a4e69518..fa705f49 100644 --- a/skimage/feature/texture.py +++ b/skimage/feature/texture.py @@ -273,8 +273,7 @@ def local_binary_pattern(image, P, R, method='default'): .. [1] Multiresolution Gray-Scale and Rotation Invariant Texture Classification with Local Binary Patterns. Timo Ojala, Matti Pietikainen, Topi Maenpaa. - http://www.rafbis.it/biplab15/images/stories/docenti/Danielriccio/\ - Articoliriferimento/LBP.pdf, 2002. + http://www.rafbis.it/biplab15/images/stories/docenti/Danielriccio/Articoliriferimento/LBP.pdf, 2002. .. [2] Face recognition with local binary patterns. Timo Ahonen, Abdenour Hadid, Matti Pietikainen, http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.214.6851, From bdd9fb08322a9b96aaed37c996e8973ae734aebc Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Tue, 16 Sep 2014 12:49:40 -0400 Subject: [PATCH 0363/1122] PEP8 formatting --- skimage/viewer/tests/test_widgets.py | 2 ++ skimage/viewer/widgets/core.py | 5 +++-- viewer_examples/plugins/tv_denoise.py | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/skimage/viewer/tests/test_widgets.py b/skimage/viewer/tests/test_widgets.py index d83f54ac..a9f84fbd 100644 --- a/skimage/viewer/tests/test_widgets.py +++ b/skimage/viewer/tests/test_widgets.py @@ -16,6 +16,7 @@ def get_image_viewer(): viewer += Plugin() return viewer + @skipif(not viewer_available) def test_check_box(): viewer = get_image_viewer() @@ -43,6 +44,7 @@ def test_combo_box(): assert_equal(str(cb.val), 'c'), assert_equal(cb.index, 2) + @skipif(not viewer_available) def test_text_widget(): viewer = get_image_viewer() diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 9a99d7e8..0eebbe8c 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -304,6 +304,7 @@ class CheckBox(BaseWidget): @val.setter def val(self, i): - self._check_box.setChecked(i) # setChecked has only two states: - # 0 = unchecked, 2 = checked + # setChecked has only two states: 0 = unchecked, 2 = checked + self._check_box.setChecked(i) + \ No newline at end of file diff --git a/viewer_examples/plugins/tv_denoise.py b/viewer_examples/plugins/tv_denoise.py index e4d06fb2..5ad527ec 100644 --- a/viewer_examples/plugins/tv_denoise.py +++ b/viewer_examples/plugins/tv_denoise.py @@ -4,8 +4,8 @@ from skimage.util import img_as_float from numpy import random, clip from skimage.viewer import ImageViewer -from skimage.viewer.widgets import (Slider, CheckBox, OKCancelButtons, - SaveButtons) +from skimage.viewer.widgets import (Slider, CheckBox, OKCancelButtons, + SaveButtons) from skimage.viewer.plugins.base import Plugin From abcd613593ef1a49f7cf912cf23e7fa5da73659a Mon Sep 17 00:00:00 2001 From: Alexey Umnov Date: Wed, 17 Sep 2014 19:13:36 +0400 Subject: [PATCH 0364/1122] Fixed ellipse symmetry and added some tests. --- skimage/draw/draw.py | 40 +++++--- skimage/draw/tests/test_draw.py | 160 +++++++++++++++++++++++++++----- 2 files changed, 166 insertions(+), 34 deletions(-) diff --git a/skimage/draw/draw.py b/skimage/draw/draw.py index e61df40c..f60a67ee 100644 --- a/skimage/draw/draw.py +++ b/skimage/draw/draw.py @@ -7,6 +7,15 @@ def _coords_inside_image(rr, cc, shape): return rr[mask], cc[mask] +def _ellipse_in_shape(shape, center, radiuses): + """Generate coordinates of points within ellipse bounded by shape.""" + y, x = np.ogrid[0:shape[0], 0:shape[1]] + cy, cx = center + ry, rx = radiuses + distances = ((y - cy) / ry) ** 2 + ((x - cx) / rx) ** 2 + return np.nonzero(distances < 1) + + def ellipse(cy, cx, yradius, xradius, shape=None): """Generate coordinates of pixels within ellipse. @@ -48,21 +57,26 @@ def ellipse(cy, cx, yradius, xradius, shape=None): """ - dr = 1 / float(yradius) - dc = 1 / float(xradius) - - r, c = np.ogrid[-1:1:dr, -1:1:dc] - rr, cc = np.nonzero(r ** 2 + c ** 2 < 1) - - rr.flags.writeable = True - cc.flags.writeable = True - rr += cy - yradius - cc += cx - xradius - + center = np.array([cy, cx]) + radiuses = np.array([yradius, xradius]) if shape is not None: - return _coords_inside_image(rr, cc, shape) + return _ellipse_in_shape(shape, center, radiuses) + else: + # rounding here is necessary to avoid rounding issues later + upper_left = np.floor(center - radiuses) - return rr, cc + shifted_center = center - upper_left + + # Shifted center is in interval [radiuses, radiuses + 1], so + # the ellipse must fit in [0, 2*radiuses + 1]. + bounding_shape = np.ceil(2 * radiuses + 1) + + rr, cc = _ellipse_in_shape(bounding_shape, shifted_center, radiuses) + rr.flags.writeable = True + cc.flags.writeable = True + rr += upper_left[0] + cc += upper_left[1] + return rr, cc def circle(cy, cx, radius, shape=None): diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py index 3cb8c2a6..dce29f6a 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -298,30 +298,134 @@ def test_circle_perimeter_aa(): assert_array_equal(img, img_) -def test_ellipse(): - img = np.zeros((15, 15), 'uint8') +def test_ellipse_trivial(): + img = np.zeros((2, 2), 'uint8') + rr, cc = ellipse(0.5, 0.5, 0.5, 0.5) + img[rr, cc] = 1 + img_correct = np.array([ + [0, 0], + [0, 0] + ]) + assert_array_equal(img, img_correct) + img = np.zeros((2, 2), 'uint8') + rr, cc = ellipse(0.5, 0.5, 1.1, 1.1) + img[rr, cc] = 1 + img_correct = np.array([ + [1, 1], + [1, 1], + ]) + assert_array_equal(img, img_correct) + + img = np.zeros((3, 3), 'uint8') + rr, cc = ellipse(1, 1, 0.9, 0.9) + img[rr, cc] = 1 + img_correct = np.array([ + [0, 0, 0], + [0, 1, 0], + [0, 0, 0], + ]) + assert_array_equal(img, img_correct) + + img = np.zeros((3, 3), 'uint8') + rr, cc = ellipse(1, 1, 1.1, 1.1) + img[rr, cc] = 1 + img_correct = np.array([ + [0, 1, 0], + [1, 1, 1], + [0, 1, 0], + ]) + assert_array_equal(img, img_correct) + + img = np.zeros((3, 3), 'uint8') + rr, cc = ellipse(1, 1, 1.5, 1.5) + img[rr, cc] = 1 + img_correct = np.array([ + [1, 1, 1], + [1, 1, 1], + [1, 1, 1], + ]) + assert_array_equal(img, img_correct) + + +def test_ellipse_generic(): + img = np.zeros((4, 4), 'uint8') + rr, cc = ellipse(1.5, 1.5, 1.1, 1.7) + img[rr, cc] = 1 + img_ = np.array([ + [0, 0, 0, 0], + [1, 1, 1, 1], + [1, 1, 1, 1], + [0, 0, 0, 0], + ]) + assert_array_equal(img, img_) + + img = np.zeros((5, 5), 'uint8') + rr, cc = ellipse(2, 2, 1.7, 1.7) + img[rr, cc] = 1 + img_ = np.array([ + [0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0], + ]) + assert_array_equal(img, img_) + + img = np.zeros((10, 10), 'uint8') + rr, cc = ellipse(5, 5, 3, 4) + img[rr, cc] = 1 + img_ = np.array([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ]) + assert_array_equal(img, img_) + + img = np.zeros((10, 10), 'uint8') + rr, cc = ellipse(4.5, 5, 3.5, 4) + img[rr, cc] = 1 + img_ = np.array([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ]) + assert_array_equal(img, img_) + + img = np.zeros((15, 15), 'uint8') rr, cc = ellipse(7, 7, 3, 7) img[rr, cc] = 1 - - img_ = np.array( - [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], - [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], - [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] - ) - + img_ = np.array([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ]) assert_array_equal(img, img_) @@ -352,6 +456,20 @@ def test_ellipse_with_shape(): assert_array_equal(img, img_) +def test_ellipse_negative(): + rr, cc = ellipse(-3, -3, 1.7, 1.7) + rr_, cc_ = np.nonzero(np.array([ + [0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0], + ])) + + assert_array_equal(rr, rr_ - 5) + assert_array_equal(cc, cc_ - 5) + + def test_ellipse_perimeter_dot_zeroangle(): # dot, angle == 0 img = np.zeros((30, 15), 'uint8') From 88a9cb4060ef936eb0afe815bcccf0a02a2e018d Mon Sep 17 00:00:00 2001 From: Alexey Umnov Date: Wed, 17 Sep 2014 20:20:26 +0400 Subject: [PATCH 0365/1122] Added float conversions for python2.x --- skimage/draw/draw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/draw/draw.py b/skimage/draw/draw.py index f60a67ee..2f03b018 100644 --- a/skimage/draw/draw.py +++ b/skimage/draw/draw.py @@ -9,7 +9,7 @@ def _coords_inside_image(rr, cc, shape): def _ellipse_in_shape(shape, center, radiuses): """Generate coordinates of points within ellipse bounded by shape.""" - y, x = np.ogrid[0:shape[0], 0:shape[1]] + y, x = np.ogrid[0:float(shape[0]), 0:float(shape[1])] cy, cx = center ry, rx = radiuses distances = ((y - cy) / ry) ** 2 + ((x - cx) / rx) ** 2 From eb7cdb5a16efd1e2c0b9f93547a7c08d8beebd70 Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Wed, 17 Sep 2014 12:29:48 -0400 Subject: [PATCH 0366/1122] remove outdated comment line --- skimage/viewer/widgets/core.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 0eebbe8c..91265633 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -304,7 +304,4 @@ class CheckBox(BaseWidget): @val.setter def val(self, i): - # setChecked has only two states: 0 = unchecked, 2 = checked - self._check_box.setChecked(i) - - \ No newline at end of file + self._check_box.setChecked(i) From 525a5f80c6a6e5079d66d7fc3e23cff934950545 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 17 Sep 2014 21:20:13 -0500 Subject: [PATCH 0367/1122] Add ndim checks in filter.edges --- skimage/filter/edges.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/skimage/filter/edges.py b/skimage/filter/edges.py index 764c7d34..bf2db375 100644 --- a/skimage/filter/edges.py +++ b/skimage/filter/edges.py @@ -110,6 +110,8 @@ def hsobel(image, mask=None): -1 -2 -1 """ + if image.ndim != 2: + raise TypeError("The input 'image' must be a two-dimensional array.") image = img_as_float(image) result = np.abs(convolve(image, HSOBEL_WEIGHTS)) return _mask_filter_result(result, mask) @@ -142,6 +144,8 @@ def vsobel(image, mask=None): 1 0 -1 """ + if image.ndim != 2: + raise TypeError("The input 'image' must be a two-dimensional array.") image = img_as_float(image) result = np.abs(convolve(image, VSOBEL_WEIGHTS)) return _mask_filter_result(result, mask) @@ -211,6 +215,8 @@ def hscharr(image, mask=None): of Kernel Based Image Derivatives. """ + if image.ndim != 2: + raise TypeError("The input 'image' must be a two-dimensional array.") image = img_as_float(image) result = np.abs(convolve(image, HSCHARR_WEIGHTS)) return _mask_filter_result(result, mask) @@ -248,6 +254,8 @@ def vscharr(image, mask=None): of Kernel Based Image Derivatives. """ + if image.ndim != 2: + raise TypeError("The input 'image' must be a two-dimensional array.") image = img_as_float(image) result = np.abs(convolve(image, VSCHARR_WEIGHTS)) return _mask_filter_result(result, mask) @@ -305,6 +313,8 @@ def hprewitt(image, mask=None): -1 -1 -1 """ + if image.ndim != 2: + raise TypeError("The input 'image' must be a two-dimensional array.") image = img_as_float(image) result = np.abs(convolve(image, HPREWITT_WEIGHTS)) return _mask_filter_result(result, mask) @@ -337,6 +347,8 @@ def vprewitt(image, mask=None): 1 0 -1 """ + if image.ndim != 2: + raise TypeError("The input 'image' must be a two-dimensional array.") image = img_as_float(image) result = np.abs(convolve(image, VPREWITT_WEIGHTS)) return _mask_filter_result(result, mask) @@ -392,6 +404,8 @@ def roberts_positive_diagonal(image, mask=None): 0 -1 """ + if image.ndim != 2: + raise TypeError("The input 'image' must be a two-dimensional array.") image = img_as_float(image) result = np.abs(convolve(image, ROBERTS_PD_WEIGHTS)) return _mask_filter_result(result, mask) @@ -426,6 +440,8 @@ def roberts_negative_diagonal(image, mask=None): -1 0 """ + if image.ndim != 2: + raise TypeError("The input 'image' must be a two-dimensional array.") image = img_as_float(image) result = np.abs(convolve(image, ROBERTS_ND_WEIGHTS)) return _mask_filter_result(result, mask) From 9c4d4fc5a4ee0d98723c8dbfa7b58a3a807561fd Mon Sep 17 00:00:00 2001 From: Alexey Umnov Date: Thu, 18 Sep 2014 16:28:32 +0400 Subject: [PATCH 0368/1122] Updated CONTRIBUTORS.txt --- CONTRIBUTORS.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index b0a82519..a166598b 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -187,4 +187,7 @@ PIL Image import and export improvements - Geoffrey French - skimage.filters.rank.windowed_histogram and plot_windowed_histogram example. \ No newline at end of file + skimage.filters.rank.windowed_histogram and plot_windowed_histogram example. + +- Alexey Umnov + skimage.draw.ellipse bug fix and tests. From 6ff1bf26eea934441a043d0a7065c309e0b99600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 18 Sep 2014 20:21:48 -0400 Subject: [PATCH 0369/1122] Move comments into if scope --- skimage/transform/_geometric.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 20d104e6..a3dd2a59 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -1124,24 +1124,24 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, out = None - # use fast Cython version for specific interpolation orders and input if order in range(4) and not map_args: + # use fast Cython version for specific interpolation orders and input matrix = None - # inverse_map is a transformation matrix as numpy array if isinstance(inverse_map, np.ndarray) and inverse_map.shape == (3, 3): + # inverse_map is a transformation matrix as numpy array matrix = inverse_map - # inverse_map is a homography elif isinstance(inverse_map, HOMOGRAPHY_TRANSFORMS): + # inverse_map is a homography matrix = inverse_map.params - # inverse_map is the inverse of a homography elif (hasattr(inverse_map, '__name__') and inverse_map.__name__ == 'inverse' and get_bound_method_class(inverse_map) \ in HOMOGRAPHY_TRANSFORMS): + # inverse_map is the inverse of a homography matrix = np.linalg.inv(six.get_method_self(inverse_map).params) if matrix is not None: @@ -1158,11 +1158,13 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, order=order, mode=mode, cval=cval)) out = np.dstack(dims) - if out is None: # use ndimage.map_coordinates - # inverse_map is a transformation matrix as numpy array, - # this is only used for order >= 4. + if out is None: + # use ndimage.map_coordinates + if (isinstance(inverse_map, np.ndarray) and inverse_map.shape == (3, 3)): + # inverse_map is a transformation matrix as numpy array, + # this is only used for order >= 4. inverse_map = ProjectiveTransform(matrix=inverse_map) if isinstance(inverse_map, np.ndarray): @@ -1181,10 +1183,10 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, def coord_map(*args): return inverse_map(*args, **map_args) - # Input image is 2D and has color channel, but output_shape is - # given for 2-D images. Automatically add the color channel - # dimensionality. if len(input_shape) == 3 and len(output_shape) == 2: + # Input image is 2D and has color channel, but output_shape is + # given for 2-D images. Automatically add the color channel + # dimensionality. output_shape = (output_shape[0], output_shape[1], input_shape[2]) From 6e26f3a7c76d52acac3cfa850a16d9571640d5cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 18 Sep 2014 20:27:38 -0400 Subject: [PATCH 0370/1122] Fix indendation of parameter description --- skimage/transform/_geometric.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index a3dd2a59..7813fc35 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -1010,7 +1010,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, e.g. `skimage.transform.SimilarityTransform`, or its inverse. - For 2-D images, you can pass a ``(3, 3)`` homogeneous transformation matrix, e.g. - `skimage.transform.SimilarityTransform.params` + `skimage.transform.SimilarityTransform.params`. - For 2-D images, a function that transforms a ``(M, 2)`` array of ``(col, row)`` coordinates in the output image to their corresponding coordinates in the input image. Extra parameters to @@ -1024,9 +1024,9 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, ``(row, col)`` coordinate in the input image. See `scipy.ndimage.map_coordinates` for further documentation. - Note, that a ``(3, 3)`` matrix is interpreted as a homogeneous - transformation matrix, so you cannot interpolate values from a 3-D - input, if the output is of shape ``(3,)``. + Note, that a ``(3, 3)`` matrix is interpreted as a homogeneous + transformation matrix, so you cannot interpolate values from a 3-D + input, if the output is of shape ``(3,)``. See example section for usage. map_args : dict, optional From 46305eeeece158366c75bbf7b0ea61045ea72c20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 18 Sep 2014 20:36:34 -0400 Subject: [PATCH 0371/1122] Remove legacy parameter --- skimage/transform/_geometric.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 7813fc35..5bc3c5e7 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -990,7 +990,7 @@ def warp_coords(coord_map, shape, dtype=np.float64): def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, - mode='constant', cval=0., reverse_map=None): + mode='constant', cval=0.): """Warp an image according to a given coordinate transformation. Parameters From fdf0908ab73a6b8c249e0bc8f765d0f5bd93c2da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 18 Sep 2014 20:49:25 -0400 Subject: [PATCH 0372/1122] Add option to skip clipping of output, and detect if input float image has negative values --- skimage/transform/_geometric.py | 26 +++++++++++++++++++------- skimage/transform/tests/test_warps.py | 11 +++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 5bc3c5e7..0264adf0 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -990,7 +990,7 @@ def warp_coords(coord_map, shape, dtype=np.float64): def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, - mode='constant', cval=0.): + mode='constant', cval=0., clip=True): """Warp an image according to a given coordinate transformation. Parameters @@ -1049,6 +1049,10 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, cval : float, optional Used in conjunction with mode 'constant', the value outside the image boundaries. + clip : bool, optional + Whether to clip the output to the float range of ``[0, 1]``, + since higher order interpolation may produce values outside the + given input range. Notes ----- @@ -1198,13 +1202,21 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, out = ndimage.map_coordinates(image, coords, prefilter=prefilter, mode=mode, order=order, cval=cval) - # The spline filters sometimes return results outside [0, 1], - # so clip to ensure valid data - clipped = np.clip(out, 0, 1) + if clip: + # The spline filters sometimes return results outside [0, 1], + # so clip to ensure valid data - if mode == 'constant' and not (0 <= cval <= 1): - clipped[out == cval] = cval + if np.min(image) < 0: + min_val = -1 + else: + min_val = 0 + max_val = 1 - out = clipped + clipped = np.clip(out, min_val, max_val) + + if mode == 'constant' and not (0 <= cval <= 1): + clipped[out == cval] = cval + + out = clipped return out diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 5c210d9b..fcdd0a87 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -55,6 +55,17 @@ def test_warp_matrix(): outx = warp(x, matrix, order=5) +def test_warp_clip(): + x = 2 * np.ones((5, 5), dtype=np.double) + matrix = np.eye(3) + + outx = warp(x, matrix, order=0, clip=False) + assert_array_almost_equal(x, outx) + + outx = warp(x, matrix, order=0, clip=True) + assert_array_almost_equal(x / 2, outx) + + def test_homography(): x = np.zeros((5, 5), dtype=np.double) x[1, 1] = 1 From f5c627493ebda4dc47ec3c96d61b4995c7f526ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 18 Sep 2014 20:52:55 -0400 Subject: [PATCH 0373/1122] Extend description of clipping behavior --- skimage/transform/_geometric.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 0264adf0..5fe2cbf0 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -1050,9 +1050,10 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, Used in conjunction with mode 'constant', the value outside the image boundaries. clip : bool, optional - Whether to clip the output to the float range of ``[0, 1]``, - since higher order interpolation may produce values outside the - given input range. + Whether to clip the output to the float range of ``[0, 1]``, or + ``[-1, 1]`` for input images with negative values. This is enabled by + default, since higher order interpolation may produce values outside + the given input range. Notes ----- From 877a7ba1099e6aa0e2e490cd941d3b61d9d6a83a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 18 Sep 2014 21:03:56 -0400 Subject: [PATCH 0374/1122] Add test for N-D warping --- skimage/transform/tests/test_warps.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index fcdd0a87..da4f0beb 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -55,6 +55,25 @@ def test_warp_matrix(): outx = warp(x, matrix, order=5) +def test_warp_nd(): + for dim in range(2, 8): + shape = dim * (5,) + + x = np.zeros(shape, dtype=np.double) + x_c = dim * (2,) + x[x_c] = 1 + refx = np.zeros(shape, dtype=np.double) + refx_c = dim * (1,) + refx[refx_c] = 1 + + coord_grid = dim * (np.arange(5),) + coords = np.array(np.meshgrid(*coord_grid)) + 1 + + outx = warp(x, coords, order=0, cval=0) + + assert_array_almost_equal(outx, refx) + + def test_warp_clip(): x = 2 * np.ones((5, 5), dtype=np.double) matrix = np.eye(3) From d367ff479b745c395f1ab130e97ea5ae35cae640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 19 Sep 2014 13:34:00 -0400 Subject: [PATCH 0375/1122] Use for mgrid for Py26 compatibility --- skimage/transform/tests/test_warps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index da4f0beb..0197c715 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -66,8 +66,8 @@ def test_warp_nd(): refx_c = dim * (1,) refx[refx_c] = 1 - coord_grid = dim * (np.arange(5),) - coords = np.array(np.meshgrid(*coord_grid)) + 1 + coord_grid = dim * (slice(0, 5, 1),) + coords = np.array(np.mgrid[coord_grid]) + 1 outx = warp(x, coords, order=0, cval=0) From c59cd1c4967b3e2b82e67ad456b0b50324d0bbc8 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 19 Sep 2014 18:50:42 -0500 Subject: [PATCH 0376/1122] Catch PIL imread problems early and generate error message --- skimage/io/_plugins/pil_plugin.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index eb6467df..9a56c0be 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -29,7 +29,11 @@ def imread(fname, dtype=None): """ im = Image.open(fname) - return pil_to_ndarray(im, dtype) + try: + return pil_to_ndarray(im, dtype) + except IOError: + raise ValueError('Could not load "%s": make sure you have library ' + 'support for "%s" files' % (fname, im.format)) def pil_to_ndarray(im, dtype=None): @@ -55,6 +59,8 @@ def pil_to_ndarray(im, dtype=None): im.shape = shape[::-1] elif 'A' in im.mode: im = im.convert('RGBA') + # this will raise an IOError if the file is not readable + im.getdata()[0] im = np.array(im, dtype=dtype) if fp is not None: fp.close() From adae753c427f64765a3158f2ba61ad6117fff80f Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 19 Sep 2014 18:52:23 -0500 Subject: [PATCH 0377/1122] Fix indent --- skimage/io/_plugins/pil_plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 9a56c0be..ac86e645 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -32,8 +32,8 @@ def imread(fname, dtype=None): try: return pil_to_ndarray(im, dtype) except IOError: - raise ValueError('Could not load "%s": make sure you have library ' - 'support for "%s" files' % (fname, im.format)) + raise ValueError('Could not load "%s": make sure you have library ' + 'support for "%s" files' % (fname, im.format)) def pil_to_ndarray(im, dtype=None): From 688f669181a95766a2992ca5b18d3928466844b6 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 19 Sep 2014 19:25:42 -0500 Subject: [PATCH 0378/1122] Perform im.getdata check up front --- skimage/io/_plugins/pil_plugin.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index ac86e645..218a9903 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -30,10 +30,13 @@ def imread(fname, dtype=None): """ im = Image.open(fname) try: - return pil_to_ndarray(im, dtype) + # this will raise an IOError if the file is not readable + im.getdata()[0] except IOError: raise ValueError('Could not load "%s": make sure you have library ' 'support for "%s" files' % (fname, im.format)) + else: + return pil_to_ndarray(im, dtype) def pil_to_ndarray(im, dtype=None): @@ -59,8 +62,6 @@ def pil_to_ndarray(im, dtype=None): im.shape = shape[::-1] elif 'A' in im.mode: im = im.convert('RGBA') - # this will raise an IOError if the file is not readable - im.getdata()[0] im = np.array(im, dtype=dtype) if fp is not None: fp.close() From 7e33ee75e8b347f5f8cfe28cb7a923c2a5b1c19c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 19 Sep 2014 19:33:22 -0500 Subject: [PATCH 0379/1122] Clean up error message. --- skimage/io/_plugins/pil_plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 218a9903..751194e5 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -33,8 +33,8 @@ def imread(fname, dtype=None): # this will raise an IOError if the file is not readable im.getdata()[0] except IOError: - raise ValueError('Could not load "%s": make sure you have library ' - 'support for "%s" files' % (fname, im.format)) + raise ValueError('Could not load "%s": make sure you have external library ' + 'installed for "%s" files' % (fname, im.format)) else: return pil_to_ndarray(im, dtype) From 53760e3d8710429da1e9611645248d936dbb2455 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 19 Sep 2014 19:39:50 -0500 Subject: [PATCH 0380/1122] Point user to Pillow docs for external libraries --- skimage/io/_plugins/pil_plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 751194e5..8ad51dba 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -33,8 +33,8 @@ def imread(fname, dtype=None): # this will raise an IOError if the file is not readable im.getdata()[0] except IOError: - raise ValueError('Could not load "%s": make sure you have external library ' - 'installed for "%s" files' % (fname, im.format)) + site = "http://pillow.readthedocs.org/en/latest/installation.html#external-libraries" + raise ValueError('Could not load "%s"\nPlease see documentation at %s' % (fname, site)) else: return pil_to_ndarray(im, dtype) From 8758ce9f93421d4aff8450cd88a5bb001af41db8 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 19 Sep 2014 19:40:41 -0500 Subject: [PATCH 0381/1122] Minor formatting of error message --- skimage/io/_plugins/pil_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 8ad51dba..f45084ce 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -34,7 +34,7 @@ def imread(fname, dtype=None): im.getdata()[0] except IOError: site = "http://pillow.readthedocs.org/en/latest/installation.html#external-libraries" - raise ValueError('Could not load "%s"\nPlease see documentation at %s' % (fname, site)) + raise ValueError('Could not load "%s"\nPlease see documentation at: %s' % (fname, site)) else: return pil_to_ndarray(im, dtype) From d6c64997cb85a4e473b3a283e3708a79ac2877f6 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 20 Sep 2014 15:43:00 -0500 Subject: [PATCH 0382/1122] Implement new assert_nD utility function --- skimage/_shared/utils.py | 9 ++++++++- skimage/filter/edges.py | 28 ++++++++++++---------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index 612a2b63..da64a32d 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -8,7 +8,7 @@ import six from ._warnings import all_warnings __all__ = ['deprecated', 'get_bound_method_class', 'all_warnings', - 'safe_as_int'] + 'safe_as_int', 'assert_nD'] class skimage_deprecation(Warning): @@ -141,3 +141,10 @@ def safe_as_int(val, atol=1e-3): "{0}, check inputs.".format(val)) return np.round(val).astype(np.int64) + + +def assert_nD(array, arg_name='image', ndim=2): + array = np.asanyarray(array) + if array.ndim != ndim: + msg = "The parameter `%s` must be a %s-dimensional array" + raise ValueError(msg % (arg_name, ndim)) diff --git a/skimage/filter/edges.py b/skimage/filter/edges.py index bf2db375..945c9ee1 100644 --- a/skimage/filter/edges.py +++ b/skimage/filter/edges.py @@ -11,6 +11,7 @@ Original author: Lee Kamentsky """ import numpy as np from skimage import img_as_float +from skimage._shared.utils import assert_nD from scipy.ndimage import convolve, binary_erosion, generate_binary_structure @@ -80,6 +81,7 @@ def sobel(image, mask=None): Note that ``scipy.ndimage.sobel`` returns a directional Sobel which has to be further processed to perform edge detection. """ + assert_nD(image) return np.sqrt(hsobel(image, mask)**2 + vsobel(image, mask)**2) @@ -110,8 +112,7 @@ def hsobel(image, mask=None): -1 -2 -1 """ - if image.ndim != 2: - raise TypeError("The input 'image' must be a two-dimensional array.") + assert_nD(image) image = img_as_float(image) result = np.abs(convolve(image, HSOBEL_WEIGHTS)) return _mask_filter_result(result, mask) @@ -144,8 +145,7 @@ def vsobel(image, mask=None): 1 0 -1 """ - if image.ndim != 2: - raise TypeError("The input 'image' must be a two-dimensional array.") + assert_nD(image) image = img_as_float(image) result = np.abs(convolve(image, VSOBEL_WEIGHTS)) return _mask_filter_result(result, mask) @@ -215,8 +215,7 @@ def hscharr(image, mask=None): of Kernel Based Image Derivatives. """ - if image.ndim != 2: - raise TypeError("The input 'image' must be a two-dimensional array.") + assert_nD(image) image = img_as_float(image) result = np.abs(convolve(image, HSCHARR_WEIGHTS)) return _mask_filter_result(result, mask) @@ -254,8 +253,7 @@ def vscharr(image, mask=None): of Kernel Based Image Derivatives. """ - if image.ndim != 2: - raise TypeError("The input 'image' must be a two-dimensional array.") + assert_nD(image) image = img_as_float(image) result = np.abs(convolve(image, VSCHARR_WEIGHTS)) return _mask_filter_result(result, mask) @@ -283,6 +281,7 @@ def prewitt(image, mask=None): Return the square root of the sum of squares of the horizontal and vertical Prewitt transforms. """ + assert_nD(image) return np.sqrt(hprewitt(image, mask)**2 + vprewitt(image, mask)**2) @@ -313,8 +312,7 @@ def hprewitt(image, mask=None): -1 -1 -1 """ - if image.ndim != 2: - raise TypeError("The input 'image' must be a two-dimensional array.") + assert_nD(image) image = img_as_float(image) result = np.abs(convolve(image, HPREWITT_WEIGHTS)) return _mask_filter_result(result, mask) @@ -347,8 +345,7 @@ def vprewitt(image, mask=None): 1 0 -1 """ - if image.ndim != 2: - raise TypeError("The input 'image' must be a two-dimensional array.") + assert_nD(image) image = img_as_float(image) result = np.abs(convolve(image, VPREWITT_WEIGHTS)) return _mask_filter_result(result, mask) @@ -371,6 +368,7 @@ def roberts(image, mask=None): output : 2-D array The Roberts' Cross edge map. """ + assert_nD(image) return np.sqrt(roberts_positive_diagonal(image, mask)**2 + roberts_negative_diagonal(image, mask)**2) @@ -404,8 +402,7 @@ def roberts_positive_diagonal(image, mask=None): 0 -1 """ - if image.ndim != 2: - raise TypeError("The input 'image' must be a two-dimensional array.") + assert_nD(image) image = img_as_float(image) result = np.abs(convolve(image, ROBERTS_PD_WEIGHTS)) return _mask_filter_result(result, mask) @@ -440,8 +437,7 @@ def roberts_negative_diagonal(image, mask=None): -1 0 """ - if image.ndim != 2: - raise TypeError("The input 'image' must be a two-dimensional array.") + assert_nD(image) image = img_as_float(image) result = np.abs(convolve(image, ROBERTS_ND_WEIGHTS)) return _mask_filter_result(result, mask) From 6a8c5e460f0a7b11e78d4b00bf707df71128606f Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 20 Sep 2014 17:04:11 -0500 Subject: [PATCH 0383/1122] Implement assert_nD in filter and feature packages --- skimage/feature/_canny.py | 5 ++--- skimage/feature/_daisy.py | 5 ++--- skimage/feature/_hog.py | 6 +++--- skimage/feature/blob.py | 11 ++++------- skimage/feature/brief.py | 2 ++ skimage/feature/censure.py | 4 +++- skimage/feature/orb.py | 4 ++++ skimage/feature/texture.py | 3 ++- skimage/feature/util.py | 5 ++--- skimage/filter/lpi_filter.py | 6 ++++++ skimage/filter/thresholding.py | 2 ++ 11 files changed, 32 insertions(+), 21 deletions(-) diff --git a/skimage/feature/_canny.py b/skimage/feature/_canny.py index 8dcd1570..fc595e45 100644 --- a/skimage/feature/_canny.py +++ b/skimage/feature/_canny.py @@ -17,6 +17,7 @@ import scipy.ndimage as ndi from scipy.ndimage import (gaussian_filter, generate_binary_structure, binary_erosion, label) from skimage import dtype_limits +from skimage._shared.utils import assert_nD def smooth_with_function_and_mask(image, function, mask): @@ -148,9 +149,7 @@ def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None): # mask by one and then mask the output. We also mask out the border points # because who knows what lies beyond the edge of the image? # - - if image.ndim != 2: - raise TypeError("The input 'image' must be a two-dimensional array.") + assert_nD(image) if low_threshold is None: low_threshold = 0.1 * dtype_limits(image)[1] diff --git a/skimage/feature/_daisy.py b/skimage/feature/_daisy.py index 3a55faf1..91429c1f 100644 --- a/skimage/feature/_daisy.py +++ b/skimage/feature/_daisy.py @@ -3,6 +3,7 @@ from scipy import sqrt, pi, arctan2, cos, sin, exp from scipy.ndimage import gaussian_filter import skimage.color from skimage import img_as_float, draw +from skimage._shared.utils import assert_nD def daisy(img, step=4, radius=15, rings=3, histograms=8, orientations=8, @@ -93,9 +94,7 @@ def daisy(img, step=4, radius=15, rings=3, histograms=8, orientations=8, .. [2] http://cvlab.epfl.ch/alumni/tola/daisy.html ''' - # Validate image format. - if img.ndim != 2: - raise ValueError('Only grey-level images are supported.') + assert_nD(img, 'img') img = img_as_float(img) diff --git a/skimage/feature/_hog.py b/skimage/feature/_hog.py index 72411308..6b496199 100644 --- a/skimage/feature/_hog.py +++ b/skimage/feature/_hog.py @@ -1,6 +1,7 @@ import numpy as np from scipy import sqrt, pi, arctan2, cos, sin from scipy.ndimage import uniform_filter +from skimage._shared.utils import assert_nD def hog(image, orientations=9, pixels_per_cell=(8, 8), @@ -59,8 +60,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), shadowing and illumination variations. """ - if image.ndim > 2: - raise ValueError("Currently only supports grey-level images") + assert_nD(image) if normalise: image = sqrt(image) @@ -79,7 +79,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), # convert uint image to float # to avoid problems with subtracting unsigned numbers in np.diff() image = image.astype('float') - + gx = np.empty(image.shape, dtype=np.double) gx[:, 0] = 0 gx[:, -1] = 0 diff --git a/skimage/feature/blob.py b/skimage/feature/blob.py index 134e7026..5bbc1de2 100644 --- a/skimage/feature/blob.py +++ b/skimage/feature/blob.py @@ -9,6 +9,7 @@ from skimage.util import img_as_float from .peak import peak_local_max from ._hessian_det_appx import _hessian_matrix_det from skimage.transform import integral_image +from skimage._shared.utils import assert_nD # This basic blob detection algorithm is based on: @@ -169,9 +170,7 @@ def blob_dog(image, min_sigma=1, max_sigma=50, sigma_ratio=1.6, threshold=2.0, ----- The radius of each blob is approximately :math:`\sqrt{2}sigma`. """ - - if image.ndim != 2: - raise ValueError("'image' must be a grayscale ") + assert_nD(image) image = img_as_float(image) @@ -275,8 +274,7 @@ def blob_log(image, min_sigma=1, max_sigma=50, num_sigma=10, threshold=.2, The radius of each blob is approximately :math:`\sqrt{2}sigma`. """ - if image.ndim != 2: - raise ValueError("'image' must be a grayscale ") + assert_nD(image) image = img_as_float(image) @@ -385,8 +383,7 @@ def blob_doh(image, min_sigma=1, max_sigma=30, num_sigma=10, threshold=0.01, due to the box filters used in the approximation of Hessian Determinant. """ - if image.ndim != 2: - raise ValueError("'image' must be grayscale ") + assert_nD(image) image = img_as_float(image) image = integral_image(image) diff --git a/skimage/feature/brief.py b/skimage/feature/brief.py index d1626f17..8d802d73 100644 --- a/skimage/feature/brief.py +++ b/skimage/feature/brief.py @@ -5,6 +5,7 @@ from .util import (DescriptorExtractor, _mask_border_keypoints, _prepare_grayscale_input_2D) from .brief_cy import _brief_loop +from skimage._shared.utils import assert_nD class BRIEF(DescriptorExtractor): @@ -137,6 +138,7 @@ class BRIEF(DescriptorExtractor): Keypoint coordinates as ``(row, col)``. """ + assert_nD(image) np.random.seed(self.sample_seed) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index eb69f115..af014436 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -9,7 +9,7 @@ from skimage.morphology import octagon, star from skimage.feature.util import _mask_border_keypoints from skimage.feature.censure_cy import _censure_dob_loop - +from skimage._shared.utils import assert_nD # The paper(Reference [1]) mentions the sizes of the Octagon shaped filter # kernel for the first seven scales only. The sizes of the later scales @@ -231,6 +231,8 @@ class CENSURE(FeatureDetector): # (4) Finally, we remove the border keypoints and return the keypoints # along with its corresponding scale. + assert_nD(image) + num_scales = self.max_scale - self.min_scale image = np.ascontiguousarray(_prepare_grayscale_input_2D(image)) diff --git a/skimage/feature/orb.py b/skimage/feature/orb.py index 2ddcf4f3..f276bd0f 100644 --- a/skimage/feature/orb.py +++ b/skimage/feature/orb.py @@ -7,6 +7,7 @@ from skimage.feature.util import (FeatureDetector, DescriptorExtractor, from skimage.feature import (corner_fast, corner_orientations, corner_peaks, corner_harris) from skimage.transform import pyramid_gaussian +from skimage._shared.utils import assert_nD from .orb_cy import _orb_loop @@ -166,6 +167,7 @@ class ORB(FeatureDetector, DescriptorExtractor): Input image. """ + assert_nD(image) pyramid = self._build_pyramid(image) @@ -237,6 +239,7 @@ class ORB(FeatureDetector, DescriptorExtractor): Corresponding orientations in radians. """ + assert_nD(image) pyramid = self._build_pyramid(image) @@ -282,6 +285,7 @@ class ORB(FeatureDetector, DescriptorExtractor): Input image. """ + assert_nD(image) pyramid = self._build_pyramid(image) diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py index fa705f49..5f6ac4a4 100644 --- a/skimage/feature/texture.py +++ b/skimage/feature/texture.py @@ -3,7 +3,7 @@ Methods to characterize image textures. """ import numpy as np - +from skimage._shared.utils import assert_nD from ._texture import _glcm_loop, _local_binary_pattern @@ -279,6 +279,7 @@ def local_binary_pattern(image, P, R, method='default'): http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.214.6851, 2004. """ + assert_nD(image) methods = { 'default': ord('D'), diff --git a/skimage/feature/util.py b/skimage/feature/util.py index 5a3e5687..5c43e3ba 100644 --- a/skimage/feature/util.py +++ b/skimage/feature/util.py @@ -1,6 +1,7 @@ import numpy as np from skimage.util import img_as_float +from skimage._shared.utils import assert_nD class FeatureDetector(object): @@ -124,9 +125,7 @@ def plot_matches(ax, image1, image2, keypoints1, keypoints2, matches, def _prepare_grayscale_input_2D(image): image = np.squeeze(image) - if image.ndim != 2: - raise ValueError("Only 2-D gray-scale images supported.") - + assert_nD(image) return img_as_float(image) diff --git a/skimage/filter/lpi_filter.py b/skimage/filter/lpi_filter.py index ef85c5cf..1224abe7 100644 --- a/skimage/filter/lpi_filter.py +++ b/skimage/filter/lpi_filter.py @@ -5,6 +5,7 @@ import numpy as np from scipy.fftpack import ifftshift +from skimage._shared.utils import assert_nD eps = np.finfo(float).eps @@ -118,6 +119,7 @@ class LPIFilter2D(object): data : (M,N) ndarray """ + assert_nD(data, 'data') F, G = self._prepare(data) out = np.dual.ifftn(F * G) out = np.abs(_centre(out, data.shape)) @@ -155,6 +157,7 @@ def forward(data, impulse_response=None, filter_params={}, >>> filtered = forward(data.coins(), filt_func) """ + assert_nD(data, 'data') if predefined_filter is None: predefined_filter = LPIFilter2D(impulse_response, **filter_params) return predefined_filter(data) @@ -184,6 +187,7 @@ def inverse(data, impulse_response=None, filter_params={}, max_gain=2, images, construct the LPIFilter2D and specify it here. """ + assert_nD(data, 'data') if predefined_filter is None: filt = LPIFilter2D(impulse_response, **filter_params) else: @@ -222,6 +226,8 @@ def wiener(data, impulse_response=None, filter_params={}, K=0.25, images, construct the LPIFilter2D and specify it here. """ + assert_nD(data, 'data') + assert_nD(K, 'K') if predefined_filter is None: filt = LPIFilter2D(impulse_response, **filter_params) else: diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index 3985e278..a07c2629 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -6,6 +6,7 @@ __all__ = ['threshold_adaptive', import numpy as np import scipy.ndimage from skimage.exposure import histogram +from skimage._shared.utils import assert_nD def threshold_adaptive(image, block_size, method='gaussian', offset=0, @@ -65,6 +66,7 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, >>> func = lambda arr: arr.mean() >>> binary_image2 = threshold_adaptive(image, 15, 'generic', param=func) """ + assert_nD(image) thresh_image = np.zeros(image.shape, 'double') if method == 'generic': scipy.ndimage.generic_filter(image, param, block_size, From 17ff39ff251dc8bd14540ada99506836a1d02554 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 20 Sep 2014 17:12:35 -0500 Subject: [PATCH 0384/1122] Allow assert_nD to take multiple dimensions --- skimage/_shared/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index da64a32d..805f3cce 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -145,6 +145,9 @@ def safe_as_int(val, atol=1e-3): def assert_nD(array, arg_name='image', ndim=2): array = np.asanyarray(array) - if array.ndim != ndim: + msg = "The parameter `%s` must be a %s-dimensional array" + if isinstance(ndim, int): + ndim = [ndim] + if not array.ndim in ndim: msg = "The parameter `%s` must be a %s-dimensional array" - raise ValueError(msg % (arg_name, ndim)) + raise ValueError(msg % (arg_name, '-or-'.join([str(n) for n in ndim])) From 028fe11515b72d9b81191af508a70e28afa6d87a Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 20 Sep 2014 17:13:57 -0500 Subject: [PATCH 0385/1122] Use assert_nD in template.py --- skimage/feature/template.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/feature/template.py b/skimage/feature/template.py index fbf95866..d06e82df 100644 --- a/skimage/feature/template.py +++ b/skimage/feature/template.py @@ -2,6 +2,7 @@ import numpy as np from scipy.signal import fftconvolve from skimage.util import pad +from skimage._shared.utils impor assert_nD def _window_sum_2d(image, window_shape): @@ -102,9 +103,8 @@ def match_template(image, template, pad_input=False, mode='constant', [ 0. , 0. , 0. , 0.125, -1. , 0.125], [ 0. , 0. , 0. , 0.125, 0.125, 0.125]], dtype=float32) """ + assert_nD(ndim=(2, 3)) - if image.ndim not in (2, 3) or template.ndim not in (2, 3): - raise ValueError("Only 2- and 3-D images supported.") if image.ndim < template.ndim: raise ValueError("Dimensionality of template must be less than or " "equal to the dimensionality of image.") From 4a270a74d79fcad73c502118db241247867eb490 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 20 Sep 2014 19:34:46 -0500 Subject: [PATCH 0386/1122] Fix syntax error --- skimage/_shared/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index 805f3cce..8143abc5 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -150,4 +150,4 @@ def assert_nD(array, arg_name='image', ndim=2): ndim = [ndim] if not array.ndim in ndim: msg = "The parameter `%s` must be a %s-dimensional array" - raise ValueError(msg % (arg_name, '-or-'.join([str(n) for n in ndim])) + raise ValueError(msg % (arg_name, '-or-'.join([str(n) for n in ndim]))) From 6a05edb9ea2234d4e981b176678990a4596515ca Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 20 Sep 2014 19:50:37 -0500 Subject: [PATCH 0387/1122] Fix syntax error and weiner check --- skimage/feature/template.py | 2 +- skimage/filter/lpi_filter.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/skimage/feature/template.py b/skimage/feature/template.py index d06e82df..c9914b14 100644 --- a/skimage/feature/template.py +++ b/skimage/feature/template.py @@ -2,7 +2,7 @@ import numpy as np from scipy.signal import fftconvolve from skimage.util import pad -from skimage._shared.utils impor assert_nD +from skimage._shared.utils import assert_nD def _window_sum_2d(image, window_shape): diff --git a/skimage/filter/lpi_filter.py b/skimage/filter/lpi_filter.py index 1224abe7..88834b78 100644 --- a/skimage/filter/lpi_filter.py +++ b/skimage/filter/lpi_filter.py @@ -227,7 +227,10 @@ def wiener(data, impulse_response=None, filter_params={}, K=0.25, """ assert_nD(data, 'data') - assert_nD(K, 'K') + + if not isinstance(K, float): + assert_nD(K, 'K') + if predefined_filter is None: filt = LPIFilter2D(impulse_response, **filter_params) else: From d2aaae40c8c29b371a5f5eccd0ea7c26bb876e9d Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 20 Sep 2014 19:58:20 -0500 Subject: [PATCH 0388/1122] Add docstring to assert_nD --- skimage/_shared/utils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index 8143abc5..92d2f3c6 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -144,6 +144,18 @@ def safe_as_int(val, atol=1e-3): def assert_nD(array, arg_name='image', ndim=2): + """ + Verify an arry meets the desired ndims. + + Parameters + ---------- + array : array-like + Input array to be validated + arg_name : str + The name of the array in the original function. + ndim : int or array-like + Allowable ndim or ndims for the array. + """ array = np.asanyarray(array) msg = "The parameter `%s` must be a %s-dimensional array" if isinstance(ndim, int): From 04f91021a97e0218e62fe516cd185d476d04da42 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 20 Sep 2014 20:09:26 -0500 Subject: [PATCH 0389/1122] Fix assert_nD call in template.py --- skimage/feature/template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/feature/template.py b/skimage/feature/template.py index c9914b14..32940e51 100644 --- a/skimage/feature/template.py +++ b/skimage/feature/template.py @@ -103,7 +103,7 @@ def match_template(image, template, pad_input=False, mode='constant', [ 0. , 0. , 0. , 0.125, -1. , 0.125], [ 0. , 0. , 0. , 0.125, 0.125, 0.125]], dtype=float32) """ - assert_nD(ndim=(2, 3)) + assert_nD(image, ndim=(2, 3)) if image.ndim < template.ndim: raise ValueError("Dimensionality of template must be less than or " From bcac1b1a6dc200f64df0b92eda583286599388d4 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 20 Sep 2014 20:09:44 -0500 Subject: [PATCH 0390/1122] Fix canny test to reflect new error type. --- skimage/feature/tests/test_canny.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/feature/tests/test_canny.py b/skimage/feature/tests/test_canny.py index 43db5037..400a7f70 100644 --- a/skimage/feature/tests/test_canny.py +++ b/skimage/feature/tests/test_canny.py @@ -60,7 +60,7 @@ class TestCanny(unittest.TestCase): self.assertTrue(point_count < 1600) def test_image_shape(self): - self.assertRaises(TypeError, F.canny, np.zeros((20, 20, 20)), 4, 0, 0) + self.assertRaises(ValueError, F.canny, np.zeros((20, 20, 20)), 4, 0, 0) def test_mask_none(self): result1 = F.canny(np.zeros((20, 20)), 4, 0, 0, np.ones((20, 20), bool)) From 2a057f7246dac04fec1721673a98cca30f5555fe Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 21 Sep 2014 05:48:58 -0500 Subject: [PATCH 0391/1122] Make ndim explicit arg for clarity and update docstring --- skimage/_shared/utils.py | 12 ++++++------ skimage/feature/_canny.py | 2 +- skimage/feature/_daisy.py | 2 +- skimage/feature/_hog.py | 2 +- skimage/feature/blob.py | 6 +++--- skimage/feature/brief.py | 2 +- skimage/feature/censure.py | 2 +- skimage/feature/orb.py | 6 +++--- skimage/feature/template.py | 2 +- skimage/feature/texture.py | 2 +- skimage/filter/_gabor.py | 3 ++- skimage/filter/edges.py | 22 +++++++++++----------- skimage/filter/lpi_filter.py | 10 +++++----- skimage/filter/rank/_percentile.py | 2 ++ skimage/filter/rank/bilateral.py | 2 ++ skimage/filter/rank/generic.py | 2 ++ skimage/filter/thresholding.py | 2 +- 17 files changed, 44 insertions(+), 37 deletions(-) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index 92d2f3c6..49cb3294 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -143,23 +143,23 @@ def safe_as_int(val, atol=1e-3): return np.round(val).astype(np.int64) -def assert_nD(array, arg_name='image', ndim=2): +def assert_nD(array, ndim, arg_name='image'): """ - Verify an arry meets the desired ndims. + Verify an array meets the desired ndims. Parameters ---------- array : array-like Input array to be validated - arg_name : str - The name of the array in the original function. - ndim : int or array-like + ndim : int or iterable of ints Allowable ndim or ndims for the array. + arg_name : str, optional + The name of the array in the original function. + """ array = np.asanyarray(array) msg = "The parameter `%s` must be a %s-dimensional array" if isinstance(ndim, int): ndim = [ndim] if not array.ndim in ndim: - msg = "The parameter `%s` must be a %s-dimensional array" raise ValueError(msg % (arg_name, '-or-'.join([str(n) for n in ndim]))) diff --git a/skimage/feature/_canny.py b/skimage/feature/_canny.py index fc595e45..a1ba2fb8 100644 --- a/skimage/feature/_canny.py +++ b/skimage/feature/_canny.py @@ -149,7 +149,7 @@ def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None): # mask by one and then mask the output. We also mask out the border points # because who knows what lies beyond the edge of the image? # - assert_nD(image) + assert_nD(image, 2) if low_threshold is None: low_threshold = 0.1 * dtype_limits(image)[1] diff --git a/skimage/feature/_daisy.py b/skimage/feature/_daisy.py index 91429c1f..d2d03ec7 100644 --- a/skimage/feature/_daisy.py +++ b/skimage/feature/_daisy.py @@ -94,7 +94,7 @@ def daisy(img, step=4, radius=15, rings=3, histograms=8, orientations=8, .. [2] http://cvlab.epfl.ch/alumni/tola/daisy.html ''' - assert_nD(img, 'img') + assert_nD(img, 2, 'img') img = img_as_float(img) diff --git a/skimage/feature/_hog.py b/skimage/feature/_hog.py index 6b496199..dd532b2e 100644 --- a/skimage/feature/_hog.py +++ b/skimage/feature/_hog.py @@ -60,7 +60,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), shadowing and illumination variations. """ - assert_nD(image) + assert_nD(image, 2) if normalise: image = sqrt(image) diff --git a/skimage/feature/blob.py b/skimage/feature/blob.py index 5bbc1de2..6431ed90 100644 --- a/skimage/feature/blob.py +++ b/skimage/feature/blob.py @@ -170,7 +170,7 @@ def blob_dog(image, min_sigma=1, max_sigma=50, sigma_ratio=1.6, threshold=2.0, ----- The radius of each blob is approximately :math:`\sqrt{2}sigma`. """ - assert_nD(image) + assert_nD(image, 2) image = img_as_float(image) @@ -274,7 +274,7 @@ def blob_log(image, min_sigma=1, max_sigma=50, num_sigma=10, threshold=.2, The radius of each blob is approximately :math:`\sqrt{2}sigma`. """ - assert_nD(image) + assert_nD(image, 2) image = img_as_float(image) @@ -383,7 +383,7 @@ def blob_doh(image, min_sigma=1, max_sigma=30, num_sigma=10, threshold=0.01, due to the box filters used in the approximation of Hessian Determinant. """ - assert_nD(image) + assert_nD(image, 2) image = img_as_float(image) image = integral_image(image) diff --git a/skimage/feature/brief.py b/skimage/feature/brief.py index 8d802d73..856fd6b7 100644 --- a/skimage/feature/brief.py +++ b/skimage/feature/brief.py @@ -138,7 +138,7 @@ class BRIEF(DescriptorExtractor): Keypoint coordinates as ``(row, col)``. """ - assert_nD(image) + assert_nD(image, 2) np.random.seed(self.sample_seed) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index af014436..1b1754a7 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -231,7 +231,7 @@ class CENSURE(FeatureDetector): # (4) Finally, we remove the border keypoints and return the keypoints # along with its corresponding scale. - assert_nD(image) + assert_nD(image, 2) num_scales = self.max_scale - self.min_scale diff --git a/skimage/feature/orb.py b/skimage/feature/orb.py index f276bd0f..0ccab314 100644 --- a/skimage/feature/orb.py +++ b/skimage/feature/orb.py @@ -167,7 +167,7 @@ class ORB(FeatureDetector, DescriptorExtractor): Input image. """ - assert_nD(image) + assert_nD(image, 2) pyramid = self._build_pyramid(image) @@ -239,7 +239,7 @@ class ORB(FeatureDetector, DescriptorExtractor): Corresponding orientations in radians. """ - assert_nD(image) + assert_nD(image, 2) pyramid = self._build_pyramid(image) @@ -285,7 +285,7 @@ class ORB(FeatureDetector, DescriptorExtractor): Input image. """ - assert_nD(image) + assert_nD(image, 2) pyramid = self._build_pyramid(image) diff --git a/skimage/feature/template.py b/skimage/feature/template.py index 32940e51..d7566310 100644 --- a/skimage/feature/template.py +++ b/skimage/feature/template.py @@ -103,7 +103,7 @@ def match_template(image, template, pad_input=False, mode='constant', [ 0. , 0. , 0. , 0.125, -1. , 0.125], [ 0. , 0. , 0. , 0.125, 0.125, 0.125]], dtype=float32) """ - assert_nD(image, ndim=(2, 3)) + assert_nD(image, (2, 3)) if image.ndim < template.ndim: raise ValueError("Dimensionality of template must be less than or " diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py index 5f6ac4a4..fc4e6ea2 100644 --- a/skimage/feature/texture.py +++ b/skimage/feature/texture.py @@ -279,7 +279,7 @@ def local_binary_pattern(image, P, R, method='default'): http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.214.6851, 2004. """ - assert_nD(image) + assert_nD(image, 2) methods = { 'default': ord('D'), diff --git a/skimage/filter/_gabor.py b/skimage/filter/_gabor.py index f766ac74..b1f3fe3b 100644 --- a/skimage/filter/_gabor.py +++ b/skimage/filter/_gabor.py @@ -1,5 +1,6 @@ import numpy as np from scipy import ndimage +from skimage._shared.utils import assert_nD __all__ = ['gabor_kernel', 'gabor_filter'] @@ -112,7 +113,7 @@ def gabor_filter(image, frequency, theta=0, bandwidth=1, sigma_x=None, .. [2] http://mplab.ucsd.edu/tutorials/gabor.pdf """ - + assert_nD(image, 2) g = gabor_kernel(frequency, theta, bandwidth, sigma_x, sigma_y, offset) filtered_real = ndimage.convolve(image, np.real(g), mode=mode, cval=cval) diff --git a/skimage/filter/edges.py b/skimage/filter/edges.py index 945c9ee1..ff3b6b03 100644 --- a/skimage/filter/edges.py +++ b/skimage/filter/edges.py @@ -81,7 +81,7 @@ def sobel(image, mask=None): Note that ``scipy.ndimage.sobel`` returns a directional Sobel which has to be further processed to perform edge detection. """ - assert_nD(image) + assert_nD(image, 2) return np.sqrt(hsobel(image, mask)**2 + vsobel(image, mask)**2) @@ -112,7 +112,7 @@ def hsobel(image, mask=None): -1 -2 -1 """ - assert_nD(image) + assert_nD(image, 2) image = img_as_float(image) result = np.abs(convolve(image, HSOBEL_WEIGHTS)) return _mask_filter_result(result, mask) @@ -145,7 +145,7 @@ def vsobel(image, mask=None): 1 0 -1 """ - assert_nD(image) + assert_nD(image, 2) image = img_as_float(image) result = np.abs(convolve(image, VSOBEL_WEIGHTS)) return _mask_filter_result(result, mask) @@ -215,7 +215,7 @@ def hscharr(image, mask=None): of Kernel Based Image Derivatives. """ - assert_nD(image) + assert_nD(image, 2) image = img_as_float(image) result = np.abs(convolve(image, HSCHARR_WEIGHTS)) return _mask_filter_result(result, mask) @@ -253,7 +253,7 @@ def vscharr(image, mask=None): of Kernel Based Image Derivatives. """ - assert_nD(image) + assert_nD(image, 2) image = img_as_float(image) result = np.abs(convolve(image, VSCHARR_WEIGHTS)) return _mask_filter_result(result, mask) @@ -281,7 +281,7 @@ def prewitt(image, mask=None): Return the square root of the sum of squares of the horizontal and vertical Prewitt transforms. """ - assert_nD(image) + assert_nD(image, 2) return np.sqrt(hprewitt(image, mask)**2 + vprewitt(image, mask)**2) @@ -312,7 +312,7 @@ def hprewitt(image, mask=None): -1 -1 -1 """ - assert_nD(image) + assert_nD(image, 2) image = img_as_float(image) result = np.abs(convolve(image, HPREWITT_WEIGHTS)) return _mask_filter_result(result, mask) @@ -345,7 +345,7 @@ def vprewitt(image, mask=None): 1 0 -1 """ - assert_nD(image) + assert_nD(image, 2) image = img_as_float(image) result = np.abs(convolve(image, VPREWITT_WEIGHTS)) return _mask_filter_result(result, mask) @@ -368,7 +368,7 @@ def roberts(image, mask=None): output : 2-D array The Roberts' Cross edge map. """ - assert_nD(image) + assert_nD(image, 2) return np.sqrt(roberts_positive_diagonal(image, mask)**2 + roberts_negative_diagonal(image, mask)**2) @@ -402,7 +402,7 @@ def roberts_positive_diagonal(image, mask=None): 0 -1 """ - assert_nD(image) + assert_nD(image, 2) image = img_as_float(image) result = np.abs(convolve(image, ROBERTS_PD_WEIGHTS)) return _mask_filter_result(result, mask) @@ -437,7 +437,7 @@ def roberts_negative_diagonal(image, mask=None): -1 0 """ - assert_nD(image) + assert_nD(image, 2) image = img_as_float(image) result = np.abs(convolve(image, ROBERTS_ND_WEIGHTS)) return _mask_filter_result(result, mask) diff --git a/skimage/filter/lpi_filter.py b/skimage/filter/lpi_filter.py index 88834b78..b02d4eb1 100644 --- a/skimage/filter/lpi_filter.py +++ b/skimage/filter/lpi_filter.py @@ -119,7 +119,7 @@ class LPIFilter2D(object): data : (M,N) ndarray """ - assert_nD(data, 'data') + assert_nD(data, 2, 'data') F, G = self._prepare(data) out = np.dual.ifftn(F * G) out = np.abs(_centre(out, data.shape)) @@ -157,7 +157,7 @@ def forward(data, impulse_response=None, filter_params={}, >>> filtered = forward(data.coins(), filt_func) """ - assert_nD(data, 'data') + assert_nD(data, 2, 'data') if predefined_filter is None: predefined_filter = LPIFilter2D(impulse_response, **filter_params) return predefined_filter(data) @@ -187,7 +187,7 @@ def inverse(data, impulse_response=None, filter_params={}, max_gain=2, images, construct the LPIFilter2D and specify it here. """ - assert_nD(data, 'data') + assert_nD(data, 2, 'data') if predefined_filter is None: filt = LPIFilter2D(impulse_response, **filter_params) else: @@ -226,10 +226,10 @@ def wiener(data, impulse_response=None, filter_params={}, K=0.25, images, construct the LPIFilter2D and specify it here. """ - assert_nD(data, 'data') + assert_nD(data, 2, 'data') if not isinstance(K, float): - assert_nD(K, 'K') + assert_nD(K, 2, 'K') if predefined_filter is None: filt = LPIFilter2D(impulse_response, **filter_params) diff --git a/skimage/filter/rank/_percentile.py b/skimage/filter/rank/_percentile.py index 9b93195b..e57f5df1 100644 --- a/skimage/filter/rank/_percentile.py +++ b/skimage/filter/rank/_percentile.py @@ -23,6 +23,7 @@ References """ import numpy as np +from skimage._shared.utils import assert_nD from . import percentile_cy from .generic import _handle_input @@ -37,6 +38,7 @@ __all__ = ['autolevel_percentile', 'gradient_percentile', def _apply(func, image, selem, out, mask, shift_x, shift_y, p0, p1, out_dtype=None): + assert_nD(image, 2) image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, out_dtype) diff --git a/skimage/filter/rank/bilateral.py b/skimage/filter/rank/bilateral.py index aeb318d1..05a5664a 100644 --- a/skimage/filter/rank/bilateral.py +++ b/skimage/filter/rank/bilateral.py @@ -25,6 +25,7 @@ References import numpy as np from skimage import img_as_ubyte +from skimage._shared.utils import assert_nD from . import bilateral_cy from .generic import _handle_input @@ -36,6 +37,7 @@ __all__ = ['mean_bilateral', 'pop_bilateral', 'sum_bilateral'] def _apply(func, image, selem, out, mask, shift_x, shift_y, s0, s1, out_dtype=None): + assert_nD(image, 2) image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, out_dtype) diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index a474d050..d0a9baec 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -19,6 +19,7 @@ References import warnings import numpy as np from skimage import img_as_ubyte +from skimage._shared.utils import assert_nD from . import generic_cy @@ -30,6 +31,7 @@ __all__ = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', 'mean', def _handle_input(image, selem, out, mask, out_dtype=None, pixel_size=1): + assert_nD(image, 2) if image.dtype not in (np.uint8, np.uint16): image = img_as_ubyte(image) diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index a07c2629..5a737293 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -66,7 +66,7 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, >>> func = lambda arr: arr.mean() >>> binary_image2 = threshold_adaptive(image, 15, 'generic', param=func) """ - assert_nD(image) + assert_nD(image, 2) thresh_image = np.zeros(image.shape, 'double') if method == 'generic': scipy.ndimage.generic_filter(image, param, block_size, From ded8fa30b9ce9d12823d81fbd45aaf76e61b9e76 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 21 Sep 2014 05:54:13 -0500 Subject: [PATCH 0392/1122] Catch a few more asserts --- skimage/feature/texture.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py index fc4e6ea2..b1e1024a 100644 --- a/skimage/feature/texture.py +++ b/skimage/feature/texture.py @@ -89,17 +89,17 @@ def greycomatrix(image, distances, angles, levels=256, symmetric=False, [0, 0, 0, 0]], dtype=uint32) """ + assert_nD(image, 2) + assert_nD(distances, 1, 'distances') + assert_nD(angles, 1, 'angles') assert levels <= 256 image = np.ascontiguousarray(image) - assert image.ndim == 2 assert image.min() >= 0 assert image.max() < levels image = image.astype(np.uint8) distances = np.ascontiguousarray(distances, dtype=np.float64) angles = np.ascontiguousarray(angles, dtype=np.float64) - assert distances.ndim == 1 - assert angles.ndim == 1 P = np.zeros((levels, levels, len(distances), len(angles)), dtype=np.uint32, order='C') @@ -179,8 +179,8 @@ def greycoprops(P, prop='contrast'): [ 1.25 , 2.75 ]]) """ + assert_nD(P, 4, 'P') - assert P.ndim == 4 (num_level, num_level2, num_dist, num_angle) = P.shape assert num_level == num_level2 assert num_dist > 0 From 2db19c624d108b64d386e2b8d60dcace66f80a78 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 21 Sep 2014 06:10:40 -0500 Subject: [PATCH 0393/1122] Add ndim argument in utility method --- skimage/feature/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/feature/util.py b/skimage/feature/util.py index 5c43e3ba..e70be719 100644 --- a/skimage/feature/util.py +++ b/skimage/feature/util.py @@ -125,7 +125,7 @@ def plot_matches(ax, image1, image2, keypoints1, keypoints2, matches, def _prepare_grayscale_input_2D(image): image = np.squeeze(image) - assert_nD(image) + assert_nD(image, 2) return img_as_float(image) From f846320d7851835991b8ff5d5b283cadea1e55e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 22 Sep 2014 08:45:51 -0400 Subject: [PATCH 0394/1122] Add option to combine tform with inverse tform --- skimage/transform/_geometric.py | 5 +++++ skimage/transform/tests/test_geometric.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 5fe2cbf0..3437945d 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -261,6 +261,10 @@ class ProjectiveTransform(GeometricTransform): else: tform = ProjectiveTransform return tform(other.params.dot(self.params)) + elif (hasattr(other, '__name__') + and other.__name__ == 'inverse' + and hasattr(get_bound_method_class(other), '_inv_matrix')): + return ProjectiveTransform(self._inv_matrix.dot(self.params)) else: raise TypeError("Cannot combine transformations of differing " "types.") @@ -789,6 +793,7 @@ TRANSFORMS = { 'projective': ProjectiveTransform, 'polynomial': PolynomialTransform, } + HOMOGRAPHY_TRANSFORMS = ( SimilarityTransform, AffineTransform, diff --git a/skimage/transform/tests/test_geometric.py b/skimage/transform/tests/test_geometric.py index 14a9305c..2d81a23d 100644 --- a/skimage/transform/tests/test_geometric.py +++ b/skimage/transform/tests/test_geometric.py @@ -218,6 +218,9 @@ def test_union(): assert_array_almost_equal(tform._matrix, tform3._matrix) assert tform.__class__ == ProjectiveTransform + tform = AffineTransform(scale=(0.1, 0.1), rotation=0.3) + assert_array_almost_equal((tform + tform.inverse).params, np.eye(3)) + def test_union_differing_types(): tform1 = SimilarityTransform() From 8432b90eb84d5ec4dea04631f087402bf61c1a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 22 Sep 2014 08:56:14 -0400 Subject: [PATCH 0395/1122] Add option to rotate around specific center --- skimage/transform/_warps.py | 15 +++++++++++---- skimage/transform/tests/test_warps.py | 11 +++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 27c92ff7..9ee5bb88 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -150,7 +150,8 @@ def rescale(image, scale, order=1, mode='constant', cval=0.): return resize(image, output_shape, order=order, mode=mode, cval=cval) -def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): +def rotate(image, angle, resize=False, order=1, mode='constant', cval=0., + center=None): """Rotate image by a certain angle around its center. Parameters @@ -180,6 +181,9 @@ def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): cval : float, optional Used in conjunction with mode 'constant', the value outside the image boundaries. + center : iterable of length 2 + The rotation center. If ``center=None``, the image is rotated around + its center, i.e. ``center=(rows / 2 - 0.5, cols / 2 - 0.5)``. Examples -------- @@ -198,10 +202,13 @@ def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): rows, cols = image.shape[0], image.shape[1] # rotation around center - translation = np.array((cols, rows)) / 2. - 0.5 - tform1 = SimilarityTransform(translation=-translation) + if center is None: + center = np.array((cols, rows)) / 2. - 0.5 + else: + center = np.asarray(center) + tform1 = SimilarityTransform(translation=-center) tform2 = SimilarityTransform(rotation=np.deg2rad(angle)) - tform3 = SimilarityTransform(translation=translation) + tform3 = SimilarityTransform(translation=center) tform = tform1 + tform2 + tform3 output_shape = None diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 0197c715..1d7ac473 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -152,6 +152,17 @@ def test_rotate_resize(): assert x45.shape == (14, 14) +def test_rotate_center(): + x = np.zeros((10, 10), dtype=np.double) + x[4, 4] = 1 + refx = np.zeros((10, 10), dtype=np.double) + refx[2, 5] = 1 + x20 = rotate(x, 20, order=0, center=(0, 0)) + assert_array_almost_equal(x20, refx) + x0 = rotate(x20, -20, order=0, center=(0, 0)) + assert_array_almost_equal(x0, x) + + def test_rescale(): # same scale factor x = np.zeros((5, 5), dtype=np.double) From 343d1d2ec3995c4787560d4f2dbdb9e23d326eb1 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 22 Sep 2014 17:54:56 +0200 Subject: [PATCH 0396/1122] Fix incorrectly documented non-optional argument --- skimage/restoration/_denoise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/restoration/_denoise.py b/skimage/restoration/_denoise.py index 260b1078..047d7388 100644 --- a/skimage/restoration/_denoise.py +++ b/skimage/restoration/_denoise.py @@ -71,7 +71,7 @@ def denoise_tv_bregman(image, weight, max_iter=100, eps=1e-3, isotropic=True): ---------- image : ndarray Input data to be denoised (converted using img_as_float`). - weight : float, optional + weight : float Denoising weight. The smaller the `weight`, the more denoising (at the expense of less similarity to the `input`). The regularization parameter `lambda` is chosen as `2 * weight`. From 5ba4b7ccd3197a59ec93e59163f034d6349034f8 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sun, 31 Aug 2014 15:19:14 +0100 Subject: [PATCH 0397/1122] Expose point_in_poly and move to measure module --- bento.info | 6 +++--- doc/logo/scipy_logo.py | 4 ++-- skimage/measure/__init__.py | 5 ++++- skimage/{morphology => measure}/_pnpoly.pyx | 12 ++++++++++-- skimage/measure/setup.py | 3 +++ .../tests/test_pnpoly.py | 19 +++++++++---------- skimage/morphology/convex_hull.py | 4 ++-- skimage/morphology/setup.py | 3 --- 8 files changed, 33 insertions(+), 23 deletions(-) rename skimage/{morphology => measure}/_pnpoly.pyx (93%) rename skimage/{morphology => measure}/tests/test_pnpoly.py (51%) diff --git a/bento.info b/bento.info index d0891272..564f1f5a 100644 --- a/bento.info +++ b/bento.info @@ -37,12 +37,12 @@ Library: skimage.io._plugins, skimage.measure, skimage.morphology, skimage.scripts, skimage.restoration, skimage.segmentation, skimage.transform, skimage.util - Extension: skimage.morphology._pnpoly - Sources: - skimage/morphology/_pnpoly.pyx Extension: skimage.io._plugins._colormixer Sources: skimage/io/_plugins/_colormixer.pyx + Extension: skimage.measure._pnpoly + Sources: + skimage/measure/_pnpoly.pyx Extension: skimage.measure._find_contours_cy Sources: skimage/measure/_find_contours_cy.pyx diff --git a/doc/logo/scipy_logo.py b/doc/logo/scipy_logo.py index c57cd95b..bc145069 100644 --- a/doc/logo/scipy_logo.py +++ b/doc/logo/scipy_logo.py @@ -3,11 +3,11 @@ Code used to trace Scipy logo. """ import numpy as np import matplotlib.pyplot as plt -import matplotlib.nxutils as nx from skimage import io from skimage import data +from skimage.measure import points_in_poly class SymmetricAnchorPoint(object): """Anchor point in a parametric curve with symmetric handles @@ -211,7 +211,7 @@ class ScipyLogo(object): y_img, x_img = np.mgrid[:h, :w] xy_points = np.column_stack((x_img.flat, y_img.flat)) - mask = nx.points_inside_poly(xy_points, xy_poly) + mask = points_in_poly(xy_points, xy_poly) return mask.reshape((h, w)) diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index e07b9789..9731d6da 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -4,6 +4,7 @@ from ._marching_cubes import (marching_cubes, mesh_surface_area, from ._regionprops import regionprops, perimeter from ._structural_similarity import structural_similarity from ._polygon import approximate_polygon, subdivide_polygon +from ._pnpoly import points_in_poly, grid_points_in_poly from ._moments import moments, moments_central, moments_normalized, moments_hu from .profile import profile_line from .fit import LineModel, CircleModel, EllipseModel, ransac @@ -30,4 +31,6 @@ __all__ = ['find_contours', 'mesh_surface_area', 'correct_mesh_orientation', 'profile_line', - 'label'] + 'label', + 'points_in_poly', + 'grid_points_in_poly'] diff --git a/skimage/morphology/_pnpoly.pyx b/skimage/measure/_pnpoly.pyx similarity index 93% rename from skimage/morphology/_pnpoly.pyx rename to skimage/measure/_pnpoly.pyx index 12b48e5d..90d990ce 100644 --- a/skimage/morphology/_pnpoly.pyx +++ b/skimage/measure/_pnpoly.pyx @@ -8,7 +8,7 @@ cimport numpy as cnp from skimage._shared.geometry cimport point_in_polygon, points_in_polygon -def grid_points_inside_poly(shape, verts): +def grid_points_in_poly(shape, verts): """Test whether points on a specified grid are inside a polygon. For each ``(r, c)`` coordinate on a grid, i.e. ``(0, 0)``, ``(0, 1)`` etc., @@ -23,6 +23,10 @@ def grid_points_inside_poly(shape, verts): or anti-clockwise. The first point may (but does not need to be) duplicated. + See Also + -------- + points_in_poly + Returns ------- mask : (M, N) ndarray of bool @@ -50,7 +54,7 @@ def grid_points_inside_poly(shape, verts): return out.view(bool) -def points_inside_poly(points, verts): +def points_in_poly(points, verts): """Test whether points lie inside a polygon. Parameters @@ -61,6 +65,10 @@ def points_inside_poly(points, verts): Vertices of the polygon, sorted either clockwise or anti-clockwise. The first point may (but does not need to be) duplicated. + See Also + -------- + grid_points_in_poly + Returns ------- mask : (N,) array of bool diff --git a/skimage/measure/setup.py b/skimage/measure/setup.py index 0fab0787..9fce3cf0 100644 --- a/skimage/measure/setup.py +++ b/skimage/measure/setup.py @@ -16,6 +16,7 @@ def configuration(parent_package='', top_path=None): cython(['_find_contours_cy.pyx'], working_path=base_path) cython(['_moments.pyx'], working_path=base_path) cython(['_marching_cubes_cy.pyx'], working_path=base_path) + cython(['_pnpoly.pyx'], working_path=base_path) config.add_extension('_ccomp', sources=['_ccomp.c'], include_dirs=[get_numpy_include_dirs()]) @@ -26,6 +27,8 @@ def configuration(parent_package='', top_path=None): config.add_extension('_marching_cubes_cy', sources=['_marching_cubes_cy.c'], include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_pnpoly', sources=['_pnpoly.c'], + include_dirs=[get_numpy_include_dirs(), '../_shared']) return config diff --git a/skimage/morphology/tests/test_pnpoly.py b/skimage/measure/tests/test_pnpoly.py similarity index 51% rename from skimage/morphology/tests/test_pnpoly.py rename to skimage/measure/tests/test_pnpoly.py index da468b08..e98b355f 100644 --- a/skimage/morphology/tests/test_pnpoly.py +++ b/skimage/measure/tests/test_pnpoly.py @@ -1,8 +1,7 @@ import numpy as np from numpy.testing import assert_array_equal -from skimage.morphology._pnpoly import points_inside_poly, \ - grid_points_inside_poly +from skimage.measure import points_in_poly, grid_points_in_poly class test_npnpoly(): @@ -11,29 +10,29 @@ class test_npnpoly(): [0, 1], [1, 1], [1, 0]]) - assert(points_inside_poly([[0.5, 0.5]], v)[0]) - assert(not points_inside_poly([[-0.1, 0.1]], v)[0]) + assert(points_in_poly([[0.5, 0.5]], v)[0]) + assert(not points_in_poly([[-0.1, 0.1]], v)[0]) def test_triangle(self): v = np.array([[0, 0], [1, 0], [0.5, 0.75]]) - assert(points_inside_poly([[0.5, 0.7]], v)[0]) - assert(not points_inside_poly([[0.5, 0.76]], v)[0]) - assert(not points_inside_poly([[0.7, 0.5]], v)[0]) + assert(points_in_poly([[0.5, 0.7]], v)[0]) + assert(not points_in_poly([[0.5, 0.76]], v)[0]) + assert(not points_in_poly([[0.7, 0.5]], v)[0]) def test_type(self): - assert(points_inside_poly([[0, 0]], [[0, 0]]).dtype == np.bool) + assert(points_in_poly([[0, 0]], [[0, 0]]).dtype == np.bool) -def test_grid_points_inside_poly(): +def test_grid_points_in_poly(): v = np.array([[0, 0], [5, 0], [5, 5]]) expected = np.tril(np.ones((5, 5), dtype=bool)) - assert_array_equal(grid_points_inside_poly((5, 5), v), + assert_array_equal(grid_points_in_poly((5, 5), v), expected) if __name__ == "__main__": diff --git a/skimage/morphology/convex_hull.py b/skimage/morphology/convex_hull.py index c5b9eb3f..edc49d4b 100644 --- a/skimage/morphology/convex_hull.py +++ b/skimage/morphology/convex_hull.py @@ -1,7 +1,7 @@ __all__ = ['convex_hull_image', 'convex_hull_object'] import numpy as np -from ._pnpoly import grid_points_inside_poly +from ..measure import grid_points_in_poly from ._convex_hull import possible_hull from ..measure._label import label from skimage.util import unique_rows @@ -72,7 +72,7 @@ def convex_hull_image(image): # For each pixel coordinate, check whether that pixel # lies inside the convex hull - mask = grid_points_inside_poly(image.shape[:2], v) + mask = grid_points_in_poly(image.shape[:2], v) return mask diff --git a/skimage/morphology/setup.py b/skimage/morphology/setup.py index f0f69764..de1a8794 100644 --- a/skimage/morphology/setup.py +++ b/skimage/morphology/setup.py @@ -15,7 +15,6 @@ def configuration(parent_package='', top_path=None): cython(['cmorph.pyx'], working_path=base_path) cython(['_watershed.pyx'], working_path=base_path) cython(['_skeletonize_cy.pyx'], working_path=base_path) - cython(['_pnpoly.pyx'], working_path=base_path) cython(['_convex_hull.pyx'], working_path=base_path) cython(['_greyreconstruct.pyx'], working_path=base_path) @@ -25,8 +24,6 @@ def configuration(parent_package='', top_path=None): include_dirs=[get_numpy_include_dirs()]) config.add_extension('_skeletonize_cy', sources=['_skeletonize_cy.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('_pnpoly', sources=['_pnpoly.c'], - include_dirs=[get_numpy_include_dirs(), '../_shared']) config.add_extension('_convex_hull', sources=['_convex_hull.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_greyreconstruct', sources=['_greyreconstruct.c'], From 4fc6f8b0649e5462f4540ba46a6c246f4ef3b078 Mon Sep 17 00:00:00 2001 From: Jonathan Helmus Date: Wed, 24 Sep 2014 10:11:38 -0500 Subject: [PATCH 0398/1122] BUG: segmentation fault when unwrapping 3d image Unwrapping a 3D image using unwrap_phase no longer causes a segmentation fault when the wrap_around parameter is True for the middle dimension. --- skimage/restoration/unwrap_3d_ljmu.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/restoration/unwrap_3d_ljmu.c b/skimage/restoration/unwrap_3d_ljmu.c index 88012f18..ebbf02ce 100644 --- a/skimage/restoration/unwrap_3d_ljmu.c +++ b/skimage/restoration/unwrap_3d_ljmu.c @@ -786,7 +786,7 @@ void verticalEDGEs(VOXELM *voxel, EDGE *edge, int volume_width, int volume_heig } voxel_pointer++; } - voxel_pointer += next_voxel + 1; + voxel_pointer += next_voxel; } } params->no_of_edges = no_of_edges; From cb7187e3ca6544d2e8babe708686dfa49045b57a Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 24 Sep 2014 16:34:50 -0500 Subject: [PATCH 0399/1122] Update build requirements and documentation --- DEPENDS.txt | 11 +++++------ requirements.txt | 7 +++---- setup.py | 42 +++++++++++++++++++++++++----------------- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/DEPENDS.txt b/DEPENDS.txt index eb7cc09c..1b301ca2 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -1,11 +1,8 @@ Build Requirements ------------------ -* `Python >= 2.5 `__ +* `Python >= 2.6 `__ * `Numpy >= 1.6 `__ -* `Cython >= 0.17 `__ - -`Matplotlib >= 1.0 `__ is needed to generate the -examples in the documentation. +* `Cython >= 0.19 `__ You can use pip to automatically install the base dependencies as follows:: @@ -13,8 +10,10 @@ You can use pip to automatically install the base dependencies as follows:: Runtime requirements -------------------- -* `SciPy >= 0.10 `__ +* `SciPy >= 0.9 `__ * `NetworkX >= 1.8 `__ +`Matplotlib >= 1.0 `__ is needed to generate the +examples in the documentation. Known build errors ------------------ diff --git a/requirements.txt b/requirements.txt index 7543d867..1af07ec6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ -cython>=0.17 -matplotlib>=1.0 +cython>=0.19 numpy>=1.6 -six>=1.3.0 -networkx>=1.8.0 +scipy>=0.9 +six>=1.3 diff --git a/setup.py b/setup.py index 640329fc..c51ef097 100755 --- a/setup.py +++ b/setup.py @@ -18,25 +18,33 @@ URL = 'http://scikit-image.org' LICENSE = 'Modified BSD' DOWNLOAD_URL = 'http://github.com/scikit-image/scikit-image' VERSION = '0.11dev' -PYTHON_VERSION = (2, 5) +PYTHON_VERSION = (2, 6) + +import re +import os +import sys + +import setuptools +from distutils.command.build_py import build_py + # These are manually checked. # These packages are sometimes installed outside of the setuptools scope -DEPENDENCIES = { - 'numpy': (1, 6), - } - -# Only require Cython if we have a developer checkout -if VERSION.endswith('dev'): - DEPENDENCIES['Cython'] = (0, 17) - - - -import os -import sys -import re -import setuptools -from distutils.command.build_py import build_py +DEPENDENCIES = {} +with open('requirements.txt') as fid: + data = fid.read().decode('utf-8', 'replace') +for line in data.splitlines(): + pkg, _, version_info = line.partition('>=') + # Only require Cython if we have a developer checkout + if pkg.lower() == 'cython' and not VERSION.endswith('dev'): + continue + version = [] + for part in re.split('\D+', version_info): + try: + version.append(int(part)) + except ValueError: + pass + DEPENDENCIES[pkg.lower()] = version def configuration(parent_package='', top_path=None): @@ -142,7 +150,7 @@ if __name__ == "__main__": configuration=configuration, install_requires=[ - "six>=1.3" + "six>=%s" % DEPENDENCIES['six'] ], packages=setuptools.find_packages(exclude=['doc']), include_package_data=True, From 214613c2a8b248331db619ea75298a0e4fdc79a2 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 26 Sep 2014 18:31:56 +1000 Subject: [PATCH 0400/1122] Run restoration test suite when run as main --- skimage/restoration/tests/test_restoration.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/skimage/restoration/tests/test_restoration.py b/skimage/restoration/tests/test_restoration.py index 915640af..0e70af56 100644 --- a/skimage/restoration/tests/test_restoration.py +++ b/skimage/restoration/tests/test_restoration.py @@ -62,3 +62,8 @@ def test_richardson_lucy(): path = pjoin(dirname(abspath(__file__)), 'camera_rl.npy') np.testing.assert_allclose(deconvolved, np.load(path), rtol=1e-3) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() From d50afed18ebdf2b312745f9dc8eb2937cf2fd084 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 26 Sep 2014 19:09:13 +1000 Subject: [PATCH 0401/1122] Add test for preserving image shape in deconv I can confirm that this test does not pass in the current master. --- skimage/restoration/tests/test_restoration.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/skimage/restoration/tests/test_restoration.py b/skimage/restoration/tests/test_restoration.py index 0e70af56..f248f613 100644 --- a/skimage/restoration/tests/test_restoration.py +++ b/skimage/restoration/tests/test_restoration.py @@ -2,6 +2,7 @@ from os.path import abspath, dirname, join as pjoin import numpy as np from scipy.signal import convolve2d +from scipy import ndimage as nd import skimage from skimage.data import camera @@ -53,6 +54,29 @@ def test_unsupervised_wiener(): rtol=1e-3) +def test_image_shape(): + """Test that shape of output image in deconvolution is same as input. + + This addresses issue #1172. + """ + point = np.zeros((5, 5), np.float) + point[2, 2] = 1. + psf = nd.gaussian_filter(point, sigma=1.) + # image shape: (45, 45), as reported in #1172 + image = skimage.img_as_float(camera()[110:155, 225:270]) # just the face + image_conv = nd.convolve(image, psf) + deconv_sup = restoration.wiener(image_conv, psf, 1) + deconv_un = restoration.unsupervised_wiener(image_conv, psf)[0] + # test the shape + np.testing.assert_equal(image.shape, deconv_sup.shape) + np.testing.assert_equal(image.shape, deconv_un.shape) + # test the reconstruction error + sup_relative_error = np.abs(deconv_sup - image) / image + un_relative_error = np.abs(deconv_un - image) / image + np.testing.assert_array_less(np.median(sup_relative_error), 0.1) + np.testing.assert_array_less(np.median(un_relative_error), 0.1) + + def test_richardson_lucy(): psf = np.ones((5, 5)) / 25 data = convolve2d(test_img, psf, 'same') From 2b7bf24c1fb8abfdd32505cf1045d2038a89a1d4 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 26 Sep 2014 19:11:32 +1000 Subject: [PATCH 0402/1122] Add shape fix for deconvolution functions --- skimage/restoration/deconvolution.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/skimage/restoration/deconvolution.py b/skimage/restoration/deconvolution.py index 51c2c705..ade2516d 100644 --- a/skimage/restoration/deconvolution.py +++ b/skimage/restoration/deconvolution.py @@ -130,7 +130,8 @@ def wiener(image, psf, balance, reg=None, is_real=True, clip=True): wiener_filter = np.conj(trans_func) / (np.abs(trans_func)**2 + balance * np.abs(reg)**2) if is_real: - deconv = uft.uirfft2(wiener_filter * uft.urfft2(image)) + deconv = uft.uirfft2(wiener_filter * uft.urfft2(image), + shape=image.shape) else: deconv = uft.uifft2(wiener_filter * uft.ufft2(image)) @@ -320,7 +321,7 @@ def unsupervised_wiener(image, psf, reg=None, user_params=None, is_real=True, # Empirical average \approx POSTMEAN Eq. 44 x_postmean = x_postmean / (iteration - params['burnin']) if is_real: - x_postmean = uft.uirfft2(x_postmean) + x_postmean = uft.uirfft2(x_postmean, shape=image.shape) else: x_postmean = uft.uifft2(x_postmean) From 141d459b71bbbe3f0a33024dd35b3e51c4f73d04 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 26 Sep 2014 19:21:59 +1000 Subject: [PATCH 0403/1122] Fix small errors in the documentation --- skimage/restoration/deconvolution.py | 47 +++++++++++++--------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/skimage/restoration/deconvolution.py b/skimage/restoration/deconvolution.py index ade2516d..60be08ca 100644 --- a/skimage/restoration/deconvolution.py +++ b/skimage/restoration/deconvolution.py @@ -36,11 +36,11 @@ def wiener(image, psf, balance, reg=None, is_real=True, clip=True): The regularisation parameter value that tunes the balance between the data adequacy that improve frequency restoration and the prior adequacy that reduce frequency restoration (to - avoid noise artifact). + avoid noise artifacts). reg : ndarray, optional The regularisation operator. The Laplacian by default. It can be an impulse response or a transfer function, as for the - psf. Shape constraint is the same than for the `psf` parameter. + psf. Shape constraint is the same as for the `psf` parameter. is_real : boolean, optional True by default. Specify if ``psf`` and ``reg`` are provided with hermitian hypothesis, that is only half of the frequency @@ -49,14 +49,13 @@ def wiener(image, psf, balance, reg=None, is_real=True, clip=True): provided as transfer function. For the hermitian property see ``uft`` module or ``np.fft.rfftn``. clip : boolean, optional - True by default. If true, pixel value of the result above 1 or - under -1 are thresholded for skimage pipeline - compatibility. + True by default. If True, pixel values of the result above 1 or + under -1 are thresholded for skimage pipeline compatibility. Returns ------- im_deconv : (M, N) ndarray - The deconvolved image + The deconvolved image. Examples -------- @@ -96,8 +95,8 @@ def wiener(image, psf, balance, reg=None, is_real=True, clip=True): prior model. By default, the prior model (Laplacian) introduce image smoothness or pixel correlation. It can also be interpreted as high-frequency penalization to compensate the instability of - the solution wrt. data (sometimes called noise amplification or - "explosive" solution). + the solution with respect to the data (sometimes called noise + amplification or "explosive" solution). Finally, the use of Fourier space implies a circulant property of :math:`H`, see [Hunt]. @@ -144,7 +143,7 @@ def wiener(image, psf, balance, reg=None, is_real=True, clip=True): def unsupervised_wiener(image, psf, reg=None, user_params=None, is_real=True, clip=True): - """Unsupervised Wiener-Hunt deconvolution + """Unsupervised Wiener-Hunt deconvolution. Return the deconvolution with a Wiener-Hunt approach, where the hyperparameters are automatically estimated. The algorithm is a @@ -154,21 +153,20 @@ def unsupervised_wiener(image, psf, reg=None, user_params=None, is_real=True, Parameters ---------- image : (M, N) ndarray - The input degraded image + The input degraded image. psf : ndarray The impulse response (input image's space) or the transfer function (Fourier space). Both are accepted. The transfer - function is recognize as being complex + function is automatically recognized as being complex (``np.iscomplexobj(psf)``). reg : ndarray, optional The regularisation operator. The Laplacian by default. It can be an impulse response or a transfer function, as for the psf. user_params : dict - dictionary of gibbs parameters. See below. + Dictionary of parameters for the Gibbs sampler. See below. clip : boolean, optional - True by default. If true, pixel value of the result above 1 or - under -1 are thresholded for skimage pipeline - compatibility. + True by default. If true, pixel values of the result above 1 or + under -1 are thresholded for skimage pipeline compatibility. Returns ------- @@ -218,10 +216,10 @@ def unsupervised_wiener(image, psf, reg=None, user_params=None, is_real=True, a sum over all the possible images weighted by their respective probability. Given the size of the problem, the exact sum is not tractable. This algorithm use of MCMC to draw image under the - posterior law. The practical idea is to only draw high probable - image since they have the biggest contribution to the mean. At the - opposite, the lowest probable image are draw less often since - their contribution are low. Finally the empirical mean of these + posterior law. The practical idea is to only draw highly probable + images since they have the biggest contribution to the mean. At the + opposite, the less probable images are drawn less often since + their contribution is low. Finally the empirical mean of these samples give us an estimation of the mean, and an exact computation with an infinite sample set. @@ -338,21 +336,20 @@ def richardson_lucy(image, psf, iterations=50, clip=True): Parameters ---------- image : ndarray - Input degraded image + Input degraded image. psf : ndarray - The point spread function + The point spread function. iterations : int - Number of iterations. This parameter play to role of + Number of iterations. This parameter plays the role of regularisation. clip : boolean, optional True by default. If true, pixel value of the result above 1 or - under -1 are thresholded for skimage pipeline - compatibility. + under -1 are thresholded for skimage pipeline compatibility. Returns ------- im_deconv : ndarray - The deconvolved image + The deconvolved image. Examples -------- From 91f1d807458543d119c2380fd0c36c45320bbbdf Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 26 Sep 2014 19:54:15 +1000 Subject: [PATCH 0404/1122] Fix typos and grammatical errors in uft.py docs --- skimage/restoration/uft.py | 153 +++++++++++++++++++------------------ 1 file changed, 78 insertions(+), 75 deletions(-) diff --git a/skimage/restoration/uft.py b/skimage/restoration/uft.py index fedcc6f6..9e452bef 100644 --- a/skimage/restoration/uft.py +++ b/skimage/restoration/uft.py @@ -3,17 +3,16 @@ """Function of unitary fourier transform and utilities -This module implement unitary fourier transform, that is ortho-normal -transform. They are especially and useful for convolution [1]: they -respect the Parseval equality, the value of the null frequency is -equal to +This module implements the unitary fourier transform, also known as +the ortho-normal transform. It is especially useful for convolution +[1], as it respects the Parseval equality. The value of the null +frequency is equal to .. math:: \frac{1}{\sqrt{n}} \sum_i x_i -or the Fourier tranform have the same energy than the original image +so the Fourier tranform has the same energy as the original image (see ``image_quad_norm`` function). The transform is applied from the -last axes for performance reason (c order array). You may use directly -the numpy.fft module for more sophisticated purpose. +last axis for performance (assuming a C-order array input). References ---------- @@ -31,14 +30,14 @@ __keywords__ = "fft, Fourier Transform, orthonormal, unitary" def ufftn(inarray, dim=None): - """N-dim unitary Fourier transform + """N-dimensional unitary Fourier transform. Parameters ---------- inarray : ndarray The array to transform. dim : int, optional - The ``dim`` last axis along wich to compute the transform. All + The last axis along which to compute the transform. All axes by default. Returns @@ -62,14 +61,14 @@ def ufftn(inarray, dim=None): def uifftn(inarray, dim=None): - """N-dim unitary inverse Fourier transform + """N-dimensional unitary inverse Fourier transform. Parameters ---------- inarray : ndarray The array to transform. dim : int, optional - The ``dim`` last axis along wich to compute the transform. All + The last axis along which to compute the transform. All axes by default. Returns @@ -93,29 +92,29 @@ def uifftn(inarray, dim=None): def urfftn(inarray, dim=None): - """N-dim real unitary Fourier transform + """N-dimensional real unitary Fourier transform. - This transform consider the Hermitian property of the transform on - real input + This transform considers the Hermitian property of the transform on + real-valued input. Parameters ---------- - inarray : ndarray + inarray : ndarray, shape (M, N, ..., P) The array to transform. dim : int, optional - The ``dim`` last axis along wich to compute the transform. All + The last axis along which to compute the transform. All axes by default. Returns ------- - outarray : ndarray (the last dim as N / 2 + 1 lenght) + outarray : ndarray, shape (M, N, ..., P / 2 + 1) The unitary N-D real Fourier transform of ``inarray``. Notes ----- The ``urfft`` functions assume an input array of real - values. Consequently, the output have an Hermitian property and - redondant values are not computed and returned. + values. Consequently, the output has a Hermitian property and + redundant values are not computed or returned. Examples -------- @@ -133,22 +132,22 @@ def urfftn(inarray, dim=None): def uirfftn(inarray, dim=None, shape=None): - """N-dim real unitary Fourier transform + """N-dimensional inverse real unitary Fourier transform. - This transform consider the Hermitian property of the transform - from complex to real real input. + This transform considers the Hermitian property of the transform + from complex to real input. Parameters ---------- inarray : ndarray The array to transform. dim : int, optional - The ``dim`` last axis along wich to compute the transform. All + The last axis along which to compute the transform. All axes by default. - shape : tuple of int + shape : tuple of int, optional The shape of the output. The shape of ``rfft`` is ambiguous in - case of odd shape. In this case, the parameter must be - used. see ``np.fft.irfftn``. + case of odd-valued input shape. In this case, this parameter + should be provided. See ``np.fft.irfftn``. Returns ------- @@ -157,9 +156,9 @@ def uirfftn(inarray, dim=None, shape=None): Notes ----- - The ``uirfft`` function assume that output array is of real - values. Consequently, the input is assumed of having an Hermitian - property and redondant values are implicit. + The ``uirfft`` function assumes that the output array is + real-valued. Consequently, the input is assumed to have a Hermitian + property and redundant values are implicit. Examples -------- @@ -177,7 +176,7 @@ def uirfftn(inarray, dim=None, shape=None): def ufft2(inarray): - """2-dim unitary Fourier transform + """2-dimensional unitary Fourier transform. Compute the Fourier transform on the last 2 axes. @@ -188,7 +187,7 @@ def ufft2(inarray): Returns ------- - outarray : ndarray (same shape than inarray) + outarray : ndarray (same shape as inarray) The unitary 2-D Fourier transform of ``inarray``. See Also @@ -199,7 +198,8 @@ def ufft2(inarray): -------- >>> input = np.ones((10, 128, 128)) >>> output = ufft2(input) - >>> np.allclose(np.sum(input[1, ...]) / np.sqrt(input[1, ...].size), output[1, 0, 0]) + >>> np.allclose(np.sum(input[1, ...]) / np.sqrt(input[1, ...].size), + ... output[1, 0, 0]) True >>> output.shape (10, 128, 128) @@ -208,7 +208,7 @@ def ufft2(inarray): def uifft2(inarray): - """2-dim inverse unitary Fourier transform + """2-dimensional inverse unitary Fourier transform. Compute the inverse Fourier transform on the last 2 axes. @@ -219,7 +219,7 @@ def uifft2(inarray): Returns ------- - outarray : ndarray (same shape than inarray) + outarray : ndarray (same shape as inarray) The unitary 2-D inverse Fourier transform of ``inarray``. See Also @@ -230,7 +230,8 @@ def uifft2(inarray): -------- >>> input = np.ones((10, 128, 128)) >>> output = uifft2(input) - >>> np.allclose(np.sum(input[1, ...]) / np.sqrt(input[1, ...].size), output[0, 0, 0]) + >>> np.allclose(np.sum(input[1, ...]) / np.sqrt(input[1, ...].size), + ... output[0, 0, 0]) True >>> output.shape (10, 128, 128) @@ -239,20 +240,20 @@ def uifft2(inarray): def urfft2(inarray): - """2-dim real unitary Fourier transform + """2-dimensional real unitary Fourier transform Compute the real Fourier transform on the last 2 axes. This - transform consider the Hermitian property of the transform from - complex to real real input. + transform considers the Hermitian property of the transform from + complex to real-valued input. Parameters ---------- - inarray : ndarray + inarray : ndarray, shape (M, N, ..., P) The array to transform. Returns ------- - outarray : ndarray (the last dim as (N - 1) *2 lenght) + outarray : ndarray, shape (M, N, ..., 2 * (P - 1)) The unitary 2-D real Fourier transform of ``inarray``. See Also @@ -263,7 +264,8 @@ def urfft2(inarray): -------- >>> input = np.ones((10, 128, 128)) >>> output = urfft2(input) - >>> np.allclose(np.sum(input[1,...]) / np.sqrt(input[1,...].size), output[1, 0, 0]) + >>> np.allclose(np.sum(input[1,...]) / np.sqrt(input[1,...].size), + ... output[1, 0, 0]) True >>> output.shape (10, 128, 65) @@ -272,20 +274,20 @@ def urfft2(inarray): def uirfft2(inarray, shape=None): - """2-dim real unitary Fourier transform + """2-dimensional inverse real unitary Fourier transform. Compute the real inverse Fourier transform on the last 2 axes. - This transform consider the Hermitian property of the transform - from complex to real real input. + This transform considers the Hermitian property of the transform + from complex to real-valued input. Parameters ---------- - inarray : ndarray + inarray : ndarray, shape (M, N, ..., P) The array to transform. Returns ------- - outarray : ndarray (the last dim as (N - 1) *2 lenght) + outarray : ndarray, shape (M, N, ..., 2 * (P - 1)) The unitary 2-D inverse real Fourier transform of ``inarray``. See Also @@ -305,14 +307,16 @@ def uirfft2(inarray, shape=None): def image_quad_norm(inarray): - """Return quadratic norm of images in Fourier space + """Return the quadratic norm of images in Fourier space. - This function detect if the image suppose the hermitian property. + This function detects whether the input image satisfies the + Hermitian property. Parameters ---------- inarray : ndarray - The images are supposed to be in the last two axes + Input image. The image data should reside in the final two + axes. Returns ------- @@ -327,40 +331,40 @@ def image_quad_norm(inarray): >>> image_quad_norm(ufft2(input)) == image_quad_norm(urfft2(input)) True """ - # If there is an hermitian symmetry + # If there is a Hermitian symmetry if inarray.shape[-1] != inarray.shape[-2]: - return 2 * np.sum(np.sum(np.abs(inarray)**2, axis=-1), axis=-1) - \ - np.sum(np.abs(inarray[..., 0])**2, axis=-1) + return (2 * np.sum(np.sum(np.abs(inarray)**2, axis=-1), axis=-1) - + np.sum(np.abs(inarray[..., 0])**2, axis=-1)) else: return np.sum(np.sum(np.abs(inarray)**2, axis=-1), axis=-1) def ir2tf(imp_resp, shape, dim=None, is_real=True): - """Compute the transfer function of IR + """Compute the transfer function of an impulse response (IR). - This function make the necessary correct zero-padding, zero - convention, correct fft2 etc... to compute the transfer function + This function makes the necessary correct zero-padding, zero + convention, correct fft2, etc... to compute the transfer function of IR. To use with unitary Fourier transform for the signal (ufftn or equivalent). Parameters ---------- imp_resp : ndarray - The impulsionnal responses. + The impulse responses. shape : tuple of int - A tuple of integer corresponding to the target shape of the - tranfert function. + A tuple of integer corresponding to the target shape of the + tranfer function. dim : int, optional - The ``dim`` last axis along wich to compute the transform. All + The last axis along which to compute the transform. All axes by default. is_real : boolean (optionnal, default True) - If True, imp_resp is supposed real and the hermissian property + If True, imp_resp is supposed real and the Hermitian property is used with rfftn Fourier transform. Returns ------- y : complex ndarray - The tranfert function of shape ``shape``. + The tranfer function of shape ``shape``. See Also -------- @@ -377,11 +381,10 @@ def ir2tf(imp_resp, shape, dim=None, is_real=True): Notes ----- - The input array can be composed of multiple dimentionnal IR with - an arbitraru number of IR. The individual IR must be accesed - through first axes. The last ``dim`` axes of space definition. The - ``dim`` parameter must be specified to compute the transform only - along these last axes. + The input array can be composed of multiple-dimensional IR with + an arbitrary number of IR. The individual IR must be accesed + through the first axes. The last ``dim`` axes contain the space + definition. """ if not dim: dim = imp_resp.ndim @@ -402,27 +405,27 @@ def ir2tf(imp_resp, shape, dim=None, is_real=True): def laplacian(ndim, shape, is_real=True): - """Return the transfer function of the Laplacian + """Return the transfer function of the Laplacian. - Laplacian is the second order difference, on line and column. + Laplacian is the second order difference, on row and column. Parameters ---------- ndim : int - The dimension of the Laplacian + The dimension of the Laplacian. shape : tuple, shape The support on which to compute the transfer function - is_real : boolean (optionnal, default True) - If True, imp_resp is supposed real and the hermissian property - is used with rfftn Fourier transform to return the transfer - function. + is_real : boolean (optional, default True) + If True, imp_resp is assumed to be real-valued and + the Hermitian property is used with rfftn Fourier transform + to return the transfer function. Returns ------- tf : array_like, complex - The transfer function + The transfer function. impr : array_like, real - The Laplacian + The Laplacian. Examples -------- From d440cade5110f88dc8925e8c137d7275b6756194 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 27 Sep 2014 05:08:02 -0500 Subject: [PATCH 0405/1122] Reorganized dependencies in DEPENDS.txt. --- DEPENDS.txt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/DEPENDS.txt b/DEPENDS.txt index 1b301ca2..027bfa49 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -3,6 +3,7 @@ Build Requirements * `Python >= 2.6 `__ * `Numpy >= 1.6 `__ * `Cython >= 0.19 `__ +* `Six >=1.3 `__ You can use pip to automatically install the base dependencies as follows:: @@ -11,9 +12,6 @@ You can use pip to automatically install the base dependencies as follows:: Runtime requirements -------------------- * `SciPy >= 0.9 `__ -* `NetworkX >= 1.8 `__ -`Matplotlib >= 1.0 `__ is needed to generate the -examples in the documentation. Known build errors ------------------ @@ -35,6 +33,11 @@ Optional Requirements You can use this scikit with the basic requirements listed above, but some functionality is only available with the following installed: +* `NetworkX >= 1.8 `__ + +* `Matplotlib >= 1.0 `__ is needed to generate the +examples in the documentation. + * `PyQt4 `__ The ``qt`` plugin that provides ``imshow(x, fancy=True)`` and `skivi`. From c92f54c54daf7c5305c8be3bd83d16ac7151c4ba Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 27 Sep 2014 05:31:45 -0500 Subject: [PATCH 0406/1122] Fix install_requires string formatting --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c51ef097..38288356 100755 --- a/setup.py +++ b/setup.py @@ -150,7 +150,7 @@ if __name__ == "__main__": configuration=configuration, install_requires=[ - "six>=%s" % DEPENDENCIES['six'] + "six>=%s" % '.'.join(str(d) for d in DEPENDENCIES['six']) ], packages=setuptools.find_packages(exclude=['doc']), include_package_data=True, From 75448b8579aa042b28bf224cf8f8dea30e005c81 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 27 Sep 2014 05:45:12 -0500 Subject: [PATCH 0407/1122] Fix python 3 unicode error --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 38288356..ec39034c 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ from distutils.command.build_py import build_py # These are manually checked. # These packages are sometimes installed outside of the setuptools scope DEPENDENCIES = {} -with open('requirements.txt') as fid: +with open('requirements.txt', 'rb') as fid: data = fid.read().decode('utf-8', 'replace') for line in data.splitlines(): pkg, _, version_info = line.partition('>=') From 984782ee54735d1f4541928f426d35b5b0da18c0 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 27 Sep 2014 05:59:48 -0500 Subject: [PATCH 0408/1122] Remove unicode decode --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ec39034c..1fda18f5 100755 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ from distutils.command.build_py import build_py # These packages are sometimes installed outside of the setuptools scope DEPENDENCIES = {} with open('requirements.txt', 'rb') as fid: - data = fid.read().decode('utf-8', 'replace') + data = fid.read() for line in data.splitlines(): pkg, _, version_info = line.partition('>=') # Only require Cython if we have a developer checkout From 075b40b92ed53f7da96da36a147d9d90c785b138 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 27 Sep 2014 06:35:35 -0500 Subject: [PATCH 0409/1122] Fix python3.3+ builds --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 1fda18f5..1a15582d 100755 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ from distutils.command.build_py import build_py # These packages are sometimes installed outside of the setuptools scope DEPENDENCIES = {} with open('requirements.txt', 'rb') as fid: - data = fid.read() + data = fid.read().decode('utf-8', 'replace') for line in data.splitlines(): pkg, _, version_info = line.partition('>=') # Only require Cython if we have a developer checkout @@ -44,7 +44,7 @@ for line in data.splitlines(): version.append(int(part)) except ValueError: pass - DEPENDENCIES[pkg.lower()] = version + DEPENDENCIES[pkg.lower()] = tuple(version) def configuration(parent_package='', top_path=None): From 78bcf17c18ec5c5895bfb6bd2153ff0f947d6dd0 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 27 Sep 2014 07:31:52 -0500 Subject: [PATCH 0410/1122] Look for version instead of version --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 1a15582d..4058b602 100755 --- a/setup.py +++ b/setup.py @@ -99,6 +99,8 @@ def check_requirements(): % PYTHON_VERSION) for package_name, min_version in DEPENDENCIES.items(): + if package_name == 'cython': + package_name = 'Cython' dep_error = False try: package = __import__(package_name) From 6a2f682552f0a09a9508fef91485400f69a91001 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 28 Sep 2014 04:56:56 -0500 Subject: [PATCH 0411/1122] Add description for networkx optional requirment --- DEPENDS.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DEPENDS.txt b/DEPENDS.txt index 027bfa49..8cf3388d 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -33,7 +33,8 @@ Optional Requirements You can use this scikit with the basic requirements listed above, but some functionality is only available with the following installed: -* `NetworkX >= 1.8 `__ +* `NetworkX >= 1.8 `__ is needed to use region +adjacency graph (RAG)-based segmentation functions. * `Matplotlib >= 1.0 `__ is needed to generate the examples in the documentation. From be5d4b19ec36c53910b422ffef4756b44e5f9484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 28 Sep 2014 18:38:35 -0400 Subject: [PATCH 0412/1122] Add stop_probability to RANSAC --- skimage/measure/fit.py | 62 ++++++++++++++++++++++++++++--- skimage/measure/tests/test_fit.py | 33 ++++++++++++++++ 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/skimage/measure/fit.py b/skimage/measure/fit.py index e0118388..d9dd11a8 100644 --- a/skimage/measure/fit.py +++ b/skimage/measure/fit.py @@ -4,6 +4,9 @@ import numpy as np from scipy import optimize +_EPSILON = np.spacing(1) + + def _check_data_dim(data, dim): if data.ndim != 2 or data.shape[1] != dim: raise ValueError('Input data must have shape (N, %d).' % dim) @@ -465,9 +468,44 @@ class EllipseModel(BaseModel): return np.concatenate((x[..., None], y[..., None]), axis=t.ndim) +def _dynamic_max_trials(n_inliers, n_samples, min_samples, probability): + """Determine number trials such that at least one outlier-free subset is + sampled for the given inlier/outlier ratio. + + Parameters + ---------- + n_inliers : int + Number of inliers in the data. + + n_samples : int + Total number of samples in the data. + + min_samples : int + Minimum number of samples chosen randomly from original data. + + probability : float + Probability (confidence) that one outlier-free sample is generated. + + Returns + ------- + trials : int + Number of trials. + + """ + inlier_ratio = n_inliers / float(n_samples) + nom = max(_EPSILON, 1 - probability) + denom = max(_EPSILON, 1 - inlier_ratio ** min_samples) + if nom == 1: + return 0 + if denom == 1: + return float('inf') + return abs(float(np.ceil(np.log(nom) / np.log(denom)))) + + def ransac(data, model_class, min_samples, residual_threshold, is_data_valid=None, is_model_valid=None, - max_trials=100, stop_sample_num=np.inf, stop_residuals_sum=0): + max_trials=100, stop_sample_num=np.inf, stop_residuals_sum=0, + stop_probability=1): """Fit a model to data with the RANSAC (random sample consensus) algorithm. RANSAC is an iterative algorithm for the robust estimation of parameters @@ -525,7 +563,18 @@ def ransac(data, model_class, min_samples, residual_threshold, stop_sample_num : int, optional Stop iteration if at least this number of inliers are found. stop_residuals_sum : float, optional - Stop iteration if sum of residuals is less equal than this threshold. + Stop iteration if sum of residuals is less than or equal to this + threshold. + stop_probability : float in range [0, 1], optional + RANSAC iteration stops if at least one outlier-free set of the + training data is sampled in RANSAC. This requires to generate at least + N samples (iterations): + + N >= log(1 - probability) / log(1 - e**m) + + where the probability (confidence) is typically set to high value such + as 0.99 and e is the current fraction of inliers w.r.t. the total + number of samples. Returns ------- @@ -616,13 +665,13 @@ def ransac(data, model_class, min_samples, residual_threshold, # make sure data is list and not tuple, so it can be modified below data = list(data) # number of samples - N = data[0].shape[0] + num_samples = data[0].shape[0] - for _ in range(max_trials): + for num_trials in range(max_trials): # choose random sample set samples = [] - random_idxs = np.random.randint(0, N, min_samples) + random_idxs = np.random.randint(0, num_samples, min_samples) for d in data: samples.append(d[random_idxs]) @@ -660,6 +709,9 @@ def ransac(data, model_class, min_samples, residual_threshold, if ( best_inlier_num >= stop_sample_num or best_inlier_residuals_sum <= stop_residuals_sum + or num_trials + >= _dynamic_max_trials(best_inlier_num, num_samples, + min_samples, stop_probability) ): break diff --git a/skimage/measure/tests/test_fit.py b/skimage/measure/tests/test_fit.py index 3ef7a0f0..af259777 100644 --- a/skimage/measure/tests/test_fit.py +++ b/skimage/measure/tests/test_fit.py @@ -2,6 +2,7 @@ import numpy as np from numpy.testing import assert_equal, assert_raises, assert_almost_equal from skimage.measure import LineModel, CircleModel, EllipseModel, ransac from skimage.transform import AffineTransform +from skimage.measure.fit import _dynamic_max_trials def test_line_model_invalid_input(): @@ -204,6 +205,38 @@ def test_ransac_is_model_valid(): assert_equal(inliers, None) +def test_ransac_dynamic_max_trials(): + # Numbers hand-calculated and confirmed on page 119 (Table 4.3) in + # Hartley, R.~I. and Zisserman, A., 2004, + # Multiple View Geometry in Computer Vision, Second Edition, + # Cambridge University Press, ISBN: 0521540518 + + # e = 0%, min_samples = X + assert_equal(_dynamic_max_trials(100, 100, 2, 0.99), 1) + + # e = 5%, min_samples = 2 + assert_equal(_dynamic_max_trials(95, 100, 2, 0.99), 2) + # e = 10%, min_samples = 2 + assert_equal(_dynamic_max_trials(90, 100, 2, 0.99), 3) + # e = 30%, min_samples = 2 + assert_equal(_dynamic_max_trials(70, 100, 2, 0.99), 7) + # e = 50%, min_samples = 2 + assert_equal(_dynamic_max_trials(50, 100, 2, 0.99), 17) + + # e = 5%, min_samples = 8 + assert_equal(_dynamic_max_trials(95, 100, 8, 0.99), 5) + # e = 10%, min_samples = 8 + assert_equal(_dynamic_max_trials(90, 100, 8, 0.99), 9) + # e = 30%, min_samples = 8 + assert_equal(_dynamic_max_trials(70, 100, 8, 0.99), 78) + # e = 50%, min_samples = 8 + assert_equal(_dynamic_max_trials(50, 100, 8, 0.99), 1177) + + # e = 0%, min_samples = 10 + assert_equal(_dynamic_max_trials(1, 100, 10, 0), 0) + assert_equal(_dynamic_max_trials(1, 100, 10, 1), float('inf')) + + def test_deprecated_params_attribute(): model = LineModel() model.params = (10, 1) From eba3d75fa9e0afe51a619a6e6ae1ddb1b6e636d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 28 Sep 2014 18:43:32 -0400 Subject: [PATCH 0413/1122] Add sanity checks for input parameters --- skimage/measure/fit.py | 9 +++++++++ skimage/measure/tests/test_fit.py | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/skimage/measure/fit.py b/skimage/measure/fit.py index d9dd11a8..669b3df3 100644 --- a/skimage/measure/fit.py +++ b/skimage/measure/fit.py @@ -659,6 +659,15 @@ def ransac(data, model_class, min_samples, residual_threshold, best_inlier_residuals_sum = np.inf best_inliers = None + if min_samples < 0: + raise ValueError("`min_samples` must be greater than zero") + + if max_trials < 0: + raise ValueError("`max_trials` must be greater than zero") + + if stop_probability < 0 or stop_probability > 1: + raise ValueError("`stop_probability` must be in range [0, 1]") + if not isinstance(data, list) and not isinstance(data, tuple): data = [data] diff --git a/skimage/measure/tests/test_fit.py b/skimage/measure/tests/test_fit.py index af259777..32d9ef22 100644 --- a/skimage/measure/tests/test_fit.py +++ b/skimage/measure/tests/test_fit.py @@ -237,6 +237,19 @@ def test_ransac_dynamic_max_trials(): assert_equal(_dynamic_max_trials(1, 100, 10, 1), float('inf')) +def test_ransac_invalid_input(): + assert_raises(ValueError, ransac, np.zeros((10, 2)), None, min_samples=-1, + residual_threshold=0) + assert_raises(ValueError, ransac, np.zeros((10, 2)), None, min_samples=2, + residual_threshold=0, max_trials=-1) + assert_raises(ValueError, ransac, np.zeros((10, 2)), None, min_samples=2, + residual_threshold=0, + stop_probability=-1) + assert_raises(ValueError, ransac, np.zeros((10, 2)), None, min_samples=2, + residual_threshold=0, + stop_probability=1.01) + + def test_deprecated_params_attribute(): model = LineModel() model.params = (10, 1) From b4a6571715ce108125d4e2b6a927ad5e2c478fc5 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 29 Sep 2014 17:13:07 +0200 Subject: [PATCH 0414/1122] Allow modification of LineTool handle style --- skimage/viewer/canvastools/linetool.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/skimage/viewer/canvastools/linetool.py b/skimage/viewer/canvastools/linetool.py index 6dbda97b..39fcd4c1 100644 --- a/skimage/viewer/canvastools/linetool.py +++ b/skimage/viewer/canvastools/linetool.py @@ -28,6 +28,9 @@ class LineTool(CanvasToolBase): Maximum pixel distance allowed when selecting control handle. line_props : dict Properties for :class:`matplotlib.lines.Line2D`. + handle_props : dict + Marker properties for the handles (also see + :class:`matplotlib.lines.Line2D`). Attributes ---------- @@ -35,7 +38,7 @@ class LineTool(CanvasToolBase): End points of line ((x1, y1), (x2, y2)). """ def __init__(self, viewer, on_move=None, on_release=None, on_enter=None, - maxdist=10, line_props=None, + maxdist=10, line_props=None, handle_props=None, **kwargs): super(LineTool, self).__init__(viewer, on_move=on_move, on_enter=on_enter, @@ -54,14 +57,16 @@ class LineTool(CanvasToolBase): self._line = lines.Line2D(x, y, visible=False, animated=True, **props) self.ax.add_line(self._line) - self._handles = ToolHandles(self.ax, x, y) + self._handles = ToolHandles(self.ax, x, y, + marker_props=handle_props) self._handles.set_visible(False) self.artists = [self._line, self._handles.artist] if on_enter is None: def on_enter(pts): x, y = np.transpose(pts) - print("length = %0.2f" % np.sqrt(np.diff(x)**2 + np.diff(y)**2)) + print("length = %0.2f" % + np.sqrt(np.diff(x)**2 + np.diff(y)**2)) self.callback_on_enter = on_enter viewer.add_tool(self) From f7ee2c28a639c99686e2b6f84848c16c44dd7075 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 29 Sep 2014 22:25:37 +0200 Subject: [PATCH 0415/1122] Add more complete documentation for widget callback --- skimage/viewer/widgets/core.py | 37 ++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 91265633..39d18261 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -81,16 +81,17 @@ class Slider(BaseWidget): Range of slider values. value : float Default slider value. If None, use midpoint between `low` and `high`. - value_type : {'float' | 'int'} + value_type : {'float' | 'int'}, optional Numeric type of slider value. - ptype : {'arg' | 'kwarg' | 'plugin'} + ptype : {'kwarg' | 'arg' | 'plugin'}, optional Parameter type. - callback : function - Callback function called in response to slider changes. This function - is typically set when the widget is added to a plugin. - orientation : {'horizontal' | 'vertical'} + callback : callable f(widget_name, value), optional + Callback function called in response to slider changes. + *Note:* This function is typically set (overridden) when the widget is + added to a plugin. + orientation : {'horizontal' | 'vertical'}, optional Slider orientation. - update_on : {'release' | 'move'} + update_on : {'release' | 'move'}, optional Control when callback function is called: on slider move or release. """ def __init__(self, name, low=0.0, high=1.0, value=None, value_type='float', @@ -217,11 +218,12 @@ class ComboBox(BaseWidget): name of the ComboBox. items: list of str Allowed parameter values. - ptype : {'arg' | 'kwarg' | 'plugin'} + ptype : {'arg' | 'kwarg' | 'plugin'}, optional Parameter type. - callback : function - Callback function called in response to ComboBox changes. This function - is typically set when the widget is added to a plugin. + callback : callable f(widget_name, value), optional + Callback function called in response to slider changes. + *Note:* This function is typically set (overridden) when the widget is + added to a plugin. """ def __init__(self, name, items, ptype='kwarg', callback=None): @@ -265,15 +267,16 @@ class CheckBox(BaseWidget): argument, it must match the name of that keyword argument (spaces are replaced with underscores). In addition, this name is displayed as the name of the CheckBox. - value: {False, True} + value: {False, True}, optional Initial state of the CheckBox. - alignment: {'center','left','right'} + alignment: {'center','left','right'}, optional Checkbox alignment - ptype : {'arg' | 'kwarg' | 'plugin'} + ptype : {'arg' | 'kwarg' | 'plugin'}, optional Parameter type - callback : function - Callback function called in response to CheckBox changes. This function - is typically set when the widget is added to a plugin. + callback : callable f(widget_name, value), optional + Callback function called in response to slider changes. + *Note:* This function is typically set (overridden) when the widget is + added to a plugin. """ def __init__(self, name, value=False, alignment='center', ptype='kwarg', From 8a914e55f133f68e98b4e2d5b35154b402ba7764 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 29 Sep 2014 22:28:42 +0200 Subject: [PATCH 0416/1122] Invoke linetool with properties to increase coverage --- skimage/viewer/tests/test_tools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/tests/test_tools.py b/skimage/viewer/tests/test_tools.py index 8cc6bd0d..b0d487a2 100644 --- a/skimage/viewer/tests/test_tools.py +++ b/skimage/viewer/tests/test_tools.py @@ -79,7 +79,8 @@ def test_line_tool(): img = data.camera() viewer = ImageViewer(img) - tool = LineTool(viewer, maxdist=10) + tool = LineTool(viewer, maxdist=10, line_props=dict(linewidth=3), + handle_props=dict(markersize=5)) tool.end_points = get_end_points(img) assert_equal(tool.end_points, np.array([[170, 256], [341, 256]])) From 7d58147fd17f8fa23906d78b68bdfc7532d58744 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 29 Sep 2014 22:32:45 +0200 Subject: [PATCH 0417/1122] Reminder to remove previous installations when upgrading on Windows --- doc/source/install.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/install.txt b/doc/source/install.txt index 18c0fdda..adaa43ab 100644 --- a/doc/source/install.txt +++ b/doc/source/install.txt @@ -3,7 +3,8 @@ Pre-built installation `Windows binaries `__ -are kindly provided by Christoph Gohlke. +are kindly provided by Christoph Gohlke (note when upgrading +that you should first uninstall any older versions). The latest stable release is also included as part of `Enthought Canopy `__, From 758bcb456eae2d864d5e2ce144ea0e1d0bf5fabb Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Tue, 30 Sep 2014 06:01:22 +0200 Subject: [PATCH 0418/1122] Improve my grammar --- doc/source/install.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/install.txt b/doc/source/install.txt index adaa43ab..975b0b54 100644 --- a/doc/source/install.txt +++ b/doc/source/install.txt @@ -3,8 +3,8 @@ Pre-built installation `Windows binaries `__ -are kindly provided by Christoph Gohlke (note when upgrading -that you should first uninstall any older versions). +are kindly provided by Christoph Gohlke (note that, when upgrading, +you should first uninstall any older versions). The latest stable release is also included as part of `Enthought Canopy `__, From 63c92537b1956400b740723f881765f9642d5f35 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Tue, 30 Sep 2014 06:18:03 +0200 Subject: [PATCH 0419/1122] Also add handle style to ThickLineTool --- skimage/viewer/canvastools/linetool.py | 8 ++++++-- skimage/viewer/tests/test_tools.py | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/skimage/viewer/canvastools/linetool.py b/skimage/viewer/canvastools/linetool.py index 39fcd4c1..f18a1915 100644 --- a/skimage/viewer/canvastools/linetool.py +++ b/skimage/viewer/canvastools/linetool.py @@ -151,6 +151,9 @@ class ThickLineTool(LineTool): Maximum pixel distance allowed when selecting control handle. line_props : dict Properties for :class:`matplotlib.lines.Line2D`. + handle_props : dict + Marker properties for the handles (also see + :class:`matplotlib.lines.Line2D`). Attributes ---------- @@ -159,13 +162,14 @@ class ThickLineTool(LineTool): """ def __init__(self, viewer, on_move=None, on_enter=None, on_release=None, - on_change=None, maxdist=10, line_props=None): + on_change=None, maxdist=10, line_props=None, handle_props=None): super(ThickLineTool, self).__init__(viewer, on_move=on_move, on_enter=on_enter, on_release=on_release, maxdist=maxdist, - line_props=line_props) + line_props=line_props, + handle_props=handle_props) if on_change is None: def on_change(*args): diff --git a/skimage/viewer/tests/test_tools.py b/skimage/viewer/tests/test_tools.py index b0d487a2..0805364c 100644 --- a/skimage/viewer/tests/test_tools.py +++ b/skimage/viewer/tests/test_tools.py @@ -104,7 +104,8 @@ def test_thick_line_tool(): img = data.camera() viewer = ImageViewer(img) - tool = ThickLineTool(viewer, maxdist=10) + tool = ThickLineTool(viewer, maxdist=10, line_props=dict(color='red'), + handle_props=dict(markersize=5)) tool.end_points = get_end_points(img) do_event(viewer, 'scroll', button='up') From ea00067b0e35cfec5b68fb2f06a19a9e98e59221 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Tue, 30 Sep 2014 06:22:24 +0200 Subject: [PATCH 0420/1122] Fix class names --- skimage/viewer/widgets/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 39d18261..4da8925b 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -221,7 +221,7 @@ class ComboBox(BaseWidget): ptype : {'arg' | 'kwarg' | 'plugin'}, optional Parameter type. callback : callable f(widget_name, value), optional - Callback function called in response to slider changes. + Callback function called in response to combobox changes. *Note:* This function is typically set (overridden) when the widget is added to a plugin. """ @@ -274,7 +274,7 @@ class CheckBox(BaseWidget): ptype : {'arg' | 'kwarg' | 'plugin'}, optional Parameter type callback : callable f(widget_name, value), optional - Callback function called in response to slider changes. + Callback function called in response to checkbox changes. *Note:* This function is typically set (overridden) when the widget is added to a plugin. """ From a2def31fca2dddce920c924fd57a211820e14b45 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 30 Sep 2014 14:51:00 +1000 Subject: [PATCH 0421/1122] Warn users of `remove_small_objects` about type When input type is `int`, we assume that the image is pre-labeled. However, when only 0 and 1 are present, it may be that the user expects us to treat it as a binary image. We don't, but now issue a warning that they might want to use a bool image. Fixes #1178. --- skimage/morphology/misc.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index fe1f27d8..3b20ef88 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -1,5 +1,6 @@ import numpy as np import functools +import warnings import scipy.ndimage as nd from .selem import _default_selem @@ -128,6 +129,10 @@ def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): "relabeling the input with `scipy.ndimage.label` or " "`skimage.morphology.label`.") + if len(component_sizes) == 2: + warnings.warn("Only one label was provided to `remove_small_objects`. " + "Did you mean to use a boolean array?") + too_small = component_sizes < min_size too_small_mask = too_small[ccs] out[too_small_mask] = 0 From 8711c745333d358b735756d3db7f0648e55936a6 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 30 Sep 2014 15:04:28 +1000 Subject: [PATCH 0422/1122] Test type warning from remove_small_objects --- skimage/morphology/tests/test_misc.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/skimage/morphology/tests/test_misc.py b/skimage/morphology/tests/test_misc.py index 94d27f64..8789b22a 100644 --- a/skimage/morphology/tests/test_misc.py +++ b/skimage/morphology/tests/test_misc.py @@ -1,5 +1,6 @@ import numpy as np -from numpy.testing import assert_array_equal, assert_equal, assert_raises +from numpy.testing import (assert_array_equal, assert_equal, assert_raises, + assert_warns) from skimage.morphology import remove_small_objects test_image = np.array([[0, 0, 0, 1, 0], @@ -55,6 +56,13 @@ def test_uint_image(): assert_array_equal(observed, expected) +def test_single_label_warning(): + image = np.array([[0, 0, 0, 1, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0]], int) + assert_warns(UserWarning, remove_small_objects, image, min_size=6) + + def test_float_input(): float_test = np.random.rand(5, 5) assert_raises(TypeError, remove_small_objects, float_test) From b44d4f7f456831639497ba1c3fd44adc2ea2c370 Mon Sep 17 00:00:00 2001 From: capitanbatata Date: Tue, 30 Sep 2014 08:32:48 +0200 Subject: [PATCH 0423/1122] Added Johannes' comments. --- skimage/color/colorconv.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index e8a95db8..47c5eac1 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -368,9 +368,9 @@ def get_xyz_coords(illuminant, observer): Parameters ---------- - illuminant : {'A', 'D50', 'D55', 'D65', 'D75', 'E'}, optional + illuminant : {"A", "D50", "D55", "D65", "D75", "E"}, optional The name of the illuminant (the function is NOT case sensitive). - observer : int, optional + observer : {"2", "10"}, optional The aperture angle of the observer. Returns @@ -748,7 +748,7 @@ def xyz2lab(xyz, illuminant="D65", observer="2"): xyz : array_like The image in XYZ format, in a 3- or 4-D array of shape ``(.., ..,[ ..,] 3)``. - illuminant : {'A', 'D50', 'D55', 'D65', 'D75', 'E'}, optional + illuminant : {"A", "D50", "D55", "D65", "D75", "E"}, optional The name of the illuminant (the function is NOT case sensitive). observer : {"2", "10"}, optional The aperture angle of the observer. @@ -816,7 +816,7 @@ def lab2xyz(lab, illuminant="D65", observer="2"): ---------- lab : array_like The image in lab format, in a 3-D array of shape ``(.., .., 3)``. - illuminant : {'A', 'D50', 'D55', 'D65', 'D75', 'E'}, optional + illuminant : {"A", "D50", "D55", "D65", "D75", "E"}, optional The name of the illuminant (the function is NOT case sensitive). observer : {"2", "10"}, optional The aperture angle of the observer. @@ -927,7 +927,7 @@ def xyz2luv(xyz, illuminant="D65", observer="2"): xyz : (M, N, [P,] 3) array_like The 3 or 4 dimensional image in XYZ format. Final dimension denotes channels. - illuminant : {'A', 'D50', 'D55', 'D65', 'D75', 'E'}, optional + illuminant : {"A", "D50", "D55", "D65", "D75", "E"}, optional The name of the illuminant (the function is NOT case sensitive). observer : {"2", "10"}, optional The aperture angle of the observer. @@ -1004,7 +1004,7 @@ def luv2xyz(luv, illuminant="D65", observer="2"): luv : (M, N, [P,] 3) array_like The 3 or 4 dimensional image in CIE-Luv format. Final dimension denotes channels. - illuminant : {'A', 'D50', 'D55', 'D65', 'D75', 'E'}, optional + illuminant : {"A", "D50", "D55", "D65", "D75", "E"}, optional The name of the illuminant (the function is NOT case sensitive). observer : {"2", "10"}, optional The aperture angle of the observer. From 15475516d9fe6d15e7b1e49228ec6b8e1f70d273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 30 Sep 2014 20:49:23 -0400 Subject: [PATCH 0424/1122] Remove extra space --- skimage/measure/fit.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/skimage/measure/fit.py b/skimage/measure/fit.py index 669b3df3..daecd63e 100644 --- a/skimage/measure/fit.py +++ b/skimage/measure/fit.py @@ -476,13 +476,10 @@ def _dynamic_max_trials(n_inliers, n_samples, min_samples, probability): ---------- n_inliers : int Number of inliers in the data. - n_samples : int Total number of samples in the data. - min_samples : int Minimum number of samples chosen randomly from original data. - probability : float Probability (confidence) that one outlier-free sample is generated. From c594416b7867c9d92105eb3300c061281f742db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 30 Sep 2014 20:55:35 -0400 Subject: [PATCH 0425/1122] Fix stop_probability description --- skimage/measure/fit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/measure/fit.py b/skimage/measure/fit.py index daecd63e..9386c9b8 100644 --- a/skimage/measure/fit.py +++ b/skimage/measure/fit.py @@ -564,7 +564,7 @@ def ransac(data, model_class, min_samples, residual_threshold, threshold. stop_probability : float in range [0, 1], optional RANSAC iteration stops if at least one outlier-free set of the - training data is sampled in RANSAC. This requires to generate at least + training data is sampled. This requires to generate at least N samples (iterations): N >= log(1 - probability) / log(1 - e**m) From 7157c197163300a39a6cce6b090e59e14d4cc98c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 30 Sep 2014 21:12:19 -0500 Subject: [PATCH 0426/1122] Make matplotlib a requirement. --- .travis.yml | 11 ++++++++--- DEPENDS.txt | 4 +--- requirements.txt | 1 + 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7f31692d..f4309e2f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,7 +45,12 @@ before_install: travis_retry conda create -n test $ENV six scipy pip flake8 nose; source activate test; fi - - travis_retry pip install coveralls pillow + - travis_retry pip install wheel; + - if [[ $END != python=2.6* ]] then + travis_retry pip install --no-index --find-links=http://wheels.scikit-image.org/ matplotlib; + else + pip install matplotlib==1.0 + end - python check_bento_build.py install: @@ -74,16 +79,16 @@ script: # https://groups.google.com/a/continuum.io/forum/#!topic/anaconda/-DLG2ZdTkw0 - if [[ $ENV == python=3.2 ]]; then travis_retry sudo apt-get install python3-pyqt4; - travis_retry pip install matplotlib==1.3.1; travis_retry pip install networkx; else - travis_retry conda install matplotlib pyqt networkx; + travis_retry conda install networkx; sudo cp ~/miniconda/envs/test/include/png* /usr/include; rm ~/miniconda/envs/test/lib/libm-2.5.so; rm ~/miniconda/envs/test/lib/libm.*; sudo apt-get install libtiff4-dev libwebp-dev xcftools; travis_retry pip install imread; travis_retry pip install pyfits; + travis_retry pip install pillow; fi - sudo apt-get install libfreeimage3 # TODO: update when SimpleITK become available on py34 or hopefully pip diff --git a/DEPENDS.txt b/DEPENDS.txt index 8cf3388d..9d6092bb 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -4,6 +4,7 @@ Build Requirements * `Numpy >= 1.6 `__ * `Cython >= 0.19 `__ * `Six >=1.3 `__ +* `Matplotlib >= 1.0 `__ You can use pip to automatically install the base dependencies as follows:: @@ -36,9 +37,6 @@ functionality is only available with the following installed: * `NetworkX >= 1.8 `__ is needed to use region adjacency graph (RAG)-based segmentation functions. -* `Matplotlib >= 1.0 `__ is needed to generate the -examples in the documentation. - * `PyQt4 `__ The ``qt`` plugin that provides ``imshow(x, fancy=True)`` and `skivi`. diff --git a/requirements.txt b/requirements.txt index 1af07ec6..9bb56a8a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ cython>=0.19 +matplotlib>=1.0 numpy>=1.6 scipy>=0.9 six>=1.3 From 75365cf893f26d31cce415a82e69e827d5b4225b Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 30 Sep 2014 21:18:15 -0500 Subject: [PATCH 0427/1122] Fix travis syntax errors --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index f4309e2f..d9884f87 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,11 +46,11 @@ before_install: source activate test; fi - travis_retry pip install wheel; - - if [[ $END != python=2.6* ]] then + - if [[ $END != python=2.6* ]]; then travis_retry pip install --no-index --find-links=http://wheels.scikit-image.org/ matplotlib; else pip install matplotlib==1.0 - end + fi - python check_bento_build.py install: From 74f26316c79159810f71daba7a423f4265692eb1 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 30 Sep 2014 21:21:58 -0500 Subject: [PATCH 0428/1122] Fix another travis syntax error --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d9884f87..8fea2c54 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,7 +49,7 @@ before_install: - if [[ $END != python=2.6* ]]; then travis_retry pip install --no-index --find-links=http://wheels.scikit-image.org/ matplotlib; else - pip install matplotlib==1.0 + pip install matplotlib==1.0; fi - python check_bento_build.py From fc2c319f225a769d34ce5131ea593127ddc4f359 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 30 Sep 2014 21:23:29 -0500 Subject: [PATCH 0429/1122] Move matplotlib to a runtime requirement --- DEPENDS.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPENDS.txt b/DEPENDS.txt index 9d6092bb..659a0655 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -4,7 +4,6 @@ Build Requirements * `Numpy >= 1.6 `__ * `Cython >= 0.19 `__ * `Six >=1.3 `__ -* `Matplotlib >= 1.0 `__ You can use pip to automatically install the base dependencies as follows:: @@ -13,6 +12,7 @@ You can use pip to automatically install the base dependencies as follows:: Runtime requirements -------------------- * `SciPy >= 0.9 `__ +* `Matplotlib >= 1.0 `__ Known build errors ------------------ From 606807ee78d7d061b12a7301554fb98cb61236b9 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 30 Sep 2014 21:23:59 -0500 Subject: [PATCH 0430/1122] Remove redundant 'usage requirements' --- DEPENDS.txt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/DEPENDS.txt b/DEPENDS.txt index 659a0655..25bdf700 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -24,11 +24,6 @@ example at ``C:\Python26\Lib\distutils\distutils.cfg``) to contain:: [build] compiler=mingw32 - -Usage Requirements ------------------- -* `Scipy `__ - Optional Requirements --------------------- You can use this scikit with the basic requirements listed above, but some From e8123999ba55a3e1e5da51b885a5c039076cdc96 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 30 Sep 2014 21:25:15 -0500 Subject: [PATCH 0431/1122] Replace PIL with matplotlib in build_versions.py --- tools/build_versions.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tools/build_versions.py b/tools/build_versions.py index 5e33dbfa..f598866e 100755 --- a/tools/build_versions.py +++ b/tools/build_versions.py @@ -4,12 +4,9 @@ from __future__ import print_function import numpy as np import scipy as sp -from PIL import Image +import matplotlib as mpl import six -for m in (np, sp, Image, six): +for m in (np, sp, mpl, six): if not m is None: - if m is Image: - print('PIL'.rjust(10), ' ', Image.VERSION) - else: - print(m.__name__.rjust(10), ' ', m.__version__) + print(m.__name__.rjust(10), ' ', m.__version__) From 83907807234890f34a4bea399c55e96b08e3f54d Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 30 Sep 2014 21:30:16 -0500 Subject: [PATCH 0432/1122] Fix typo in travis preventing mpl 1.0 from installing --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8fea2c54..191a60e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,7 +46,7 @@ before_install: source activate test; fi - travis_retry pip install wheel; - - if [[ $END != python=2.6* ]]; then + - if [[ $ENV != python=2.6* ]]; then travis_retry pip install --no-index --find-links=http://wheels.scikit-image.org/ matplotlib; else pip install matplotlib==1.0; From cb3b5c5e4c50a0c4dcfab34d60337fc43d8690de Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 30 Sep 2014 21:36:26 -0500 Subject: [PATCH 0433/1122] Install matplotlib build dependencies --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 191a60e0..db73c829 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,7 @@ before_install: - export SPACER="\n\n\n\n\n\n\n\n\n\n*************************\n\n" - sh -e /etc/init.d/xvfb start - sudo apt-get update + - sudo apt-get build-dep python-matplotlib # Python 3.2 is not supported by Miniconda, so we use the package manager for that run. # NumPy has a bug in python 3 that is only fixed in the latest version, From 36e5628e4f444b69a4817aa3cd90dd7183a5057c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 1 Oct 2014 05:02:24 -0500 Subject: [PATCH 0434/1122] Fix matplotlib base version and install libjpeg-dev --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index db73c829..0e90238e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,7 @@ before_install: - export SPACER="\n\n\n\n\n\n\n\n\n\n*************************\n\n" - sh -e /etc/init.d/xvfb start - sudo apt-get update - - sudo apt-get build-dep python-matplotlib + - sudo apt-get build-dep python-matplotlib libjpeg-dev # Python 3.2 is not supported by Miniconda, so we use the package manager for that run. # NumPy has a bug in python 3 that is only fixed in the latest version, @@ -50,7 +50,7 @@ before_install: - if [[ $ENV != python=2.6* ]]; then travis_retry pip install --no-index --find-links=http://wheels.scikit-image.org/ matplotlib; else - pip install matplotlib==1.0; + pip install matplotlib==1.0.1; fi - python check_bento_build.py From 3b1b3f64af3f9d62fc8bde4b83b2c558c3010015 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 1 Oct 2014 05:02:59 -0500 Subject: [PATCH 0435/1122] Install libjpeg-dev, not build-dep libjpeg-dev --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0e90238e..33b6059b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,8 @@ before_install: - export SPACER="\n\n\n\n\n\n\n\n\n\n*************************\n\n" - sh -e /etc/init.d/xvfb start - sudo apt-get update - - sudo apt-get build-dep python-matplotlib libjpeg-dev + - sudo apt-get build-dep python-matplotlib + - sudo apt-get install libjpeg-dev # Python 3.2 is not supported by Miniconda, so we use the package manager for that run. # NumPy has a bug in python 3 that is only fixed in the latest version, From 15fb553cfb77a540232eff3e4faa1590c4ba5157 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 1 Oct 2014 05:28:08 -0500 Subject: [PATCH 0436/1122] Try explicit apt installs for matplotlib --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 33b6059b..62d55886 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,8 +25,7 @@ before_install: - export SPACER="\n\n\n\n\n\n\n\n\n\n*************************\n\n" - sh -e /etc/init.d/xvfb start - sudo apt-get update - - sudo apt-get build-dep python-matplotlib - - sudo apt-get install libjpeg-dev + - sudo apt-get install python-dev libjpeg-dev libfreetype6-dev zlib1g-dev # Python 3.2 is not supported by Miniconda, so we use the package manager for that run. # NumPy has a bug in python 3 that is only fixed in the latest version, From 06fd01782816305789083f723ad7a3498a4e415b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 1 Oct 2014 15:30:27 -0400 Subject: [PATCH 0437/1122] Improve description of stop_probability --- skimage/measure/fit.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/skimage/measure/fit.py b/skimage/measure/fit.py index 9386c9b8..7de90291 100644 --- a/skimage/measure/fit.py +++ b/skimage/measure/fit.py @@ -564,14 +564,15 @@ def ransac(data, model_class, min_samples, residual_threshold, threshold. stop_probability : float in range [0, 1], optional RANSAC iteration stops if at least one outlier-free set of the - training data is sampled. This requires to generate at least - N samples (iterations): + training data is sampled with ``probability >= stop_probability``, + depending on the current best model's inlier ratio and the number + of trials. This requires to generate at least N samples (trials): N >= log(1 - probability) / log(1 - e**m) - where the probability (confidence) is typically set to high value such - as 0.99 and e is the current fraction of inliers w.r.t. the total - number of samples. + where the probability (confidence) is typically set to a high value + such as 0.99, and e is the current fraction of inliers w.r.t. the + total number of samples. Returns ------- From febef86bbbd75038c548d1ec9fd7f5bc372872a6 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 4 Oct 2014 20:19:58 +0530 Subject: [PATCH 0438/1122] added non in-place merge --- doc/examples/plot_rag.py | 4 ++-- skimage/graph/rag.py | 32 ++++++++++++++++++++++++++------ skimage/graph/tests/test_rag.py | 4 ++-- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/doc/examples/plot_rag.py b/doc/examples/plot_rag.py index b26a266d..f360af52 100644 --- a/doc/examples/plot_rag.py +++ b/doc/examples/plot_rag.py @@ -75,7 +75,7 @@ display(g, "Original Graph") g.merge_nodes(1, 3) display(g, "Merged with default (min)") -gc.merge_nodes(1, 3, weight_func=max_edge) -display(gc, "Merged with max") +gc.merge_nodes(1, 3, weight_func=max_edge, in_place=False) +display(gc, "Merged with max without in_place") plt.show() diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 5995fb5d..8f605b1e 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -59,9 +59,9 @@ class RAG(nx.Graph): `networx.Graph `_ """ - def merge_nodes(self, src, dst, weight_func=min_weight, extra_arguments=[], - extra_keywords={}): - """Merge node `src` into `dst`. + def merge_nodes(self, src, dst, weight_func=min_weight, in_place=True, + extra_arguments=[], extra_keywords={}): + """Merge node `src` and `dst`. The new combined node is adjacent to all the neighbors of `src` and `dst`. `weight_func` is called to decide the weight of edges @@ -78,24 +78,44 @@ class RAG(nx.Graph): **extra_keywords)`. `src`, `dst` and `n` are IDs of vertices in the RAG object which is in turn a subclass of `networkx.Graph`. + in_place : bool + If set to `True`, the merged node has the id `dst`, else merged + node has a new id which is returned. extra_arguments : sequence, optional The sequence of extra positional arguments passed to `weight_func`. extra_keywords : dictionary, optional The dict of keyword arguments passed to the `weight_func`. + Returns + ------- + id : int + The id of the new node if `in_place` is `True`. """ src_nbrs = set(self.neighbors(src)) dst_nbrs = set(self.neighbors(dst)) neighbors = (src_nbrs & dst_nbrs) - set([src, dst]) + if not in_place: + new = self.number_of_nodes() + 1 + self.add_node(new) for neighbor in neighbors: w = weight_func(self, src, dst, neighbor, *extra_arguments, **extra_keywords) - self.add_edge(neighbor, dst, weight=w) + if in_place: + self.add_edge(neighbor, dst, weight=w) + else: + self.add_edge(neighbor, new, weight=w) - self.node[dst]['labels'] += self.node[src]['labels'] - self.remove_node(src) + if in_place: + self.node[dst]['labels'] += self.node[src]['labels'] + self.remove_node(src) + else: + self.node[new]['labels'] = (self.node[src]['labels'] + + self.node[dst]['labels']) + self.remove_node(src) + self.remove_node(dst) + return new def _add_edge_filter(values, graph): diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 26f5a85c..04b82ff7 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -41,8 +41,8 @@ def test_rag_merge(): assert gc.edge[1][2]['weight'] == 20 assert gc.edge[2][3]['weight'] == 40 - g.merge_nodes(1, 4) - g.merge_nodes(2, 3) + g.merge_nodes(1, 4, in_place=True) + g.merge_nodes(2, 3, in_place=True) g.merge_nodes(3, 4) assert sorted(g.node[4]['labels']) == list(range(5)) assert g.edges() == [] From 3a9df73590ae4323bbcfa47588cdcf02f13a743d Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 4 Oct 2014 22:33:10 +0530 Subject: [PATCH 0439/1122] Corrected new id logic --- skimage/graph/rag.py | 6 +++++- skimage/graph/tests/test_rag.py | 8 ++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 8f605b1e..26808b0a 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -15,6 +15,7 @@ from scipy.ndimage import filters from scipy import ndimage as nd import math from .. import draw, measure, segmentation, util, color +import random try: from matplotlib import colors from matplotlib import cm @@ -96,7 +97,10 @@ class RAG(nx.Graph): dst_nbrs = set(self.neighbors(dst)) neighbors = (src_nbrs & dst_nbrs) - set([src, dst]) if not in_place: - new = self.number_of_nodes() + 1 + # Randomly select an id + new = random.randint(1, 99999999) + while new in self: + new = random.randint(1, 99999999) self.add_node(new) for neighbor in neighbors: diff --git a/skimage/graph/tests/test_rag.py b/skimage/graph/tests/test_rag.py index 04b82ff7..35eb5c38 100644 --- a/skimage/graph/tests/test_rag.py +++ b/skimage/graph/tests/test_rag.py @@ -41,10 +41,10 @@ def test_rag_merge(): assert gc.edge[1][2]['weight'] == 20 assert gc.edge[2][3]['weight'] == 40 - g.merge_nodes(1, 4, in_place=True) - g.merge_nodes(2, 3, in_place=True) - g.merge_nodes(3, 4) - assert sorted(g.node[4]['labels']) == list(range(5)) + g.merge_nodes(1, 4) + g.merge_nodes(2, 3) + n = g.merge_nodes(3, 4, in_place=False) + assert sorted(g.node[n]['labels']) == list(range(5)) assert g.edges() == [] From 02fec246ea86901fd20e7f13a1cb4048aef2f6e8 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 4 Oct 2014 22:37:12 +0530 Subject: [PATCH 0440/1122] change in random number max logic --- skimage/graph/rag.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 26808b0a..5820250c 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -96,11 +96,12 @@ class RAG(nx.Graph): src_nbrs = set(self.neighbors(src)) dst_nbrs = set(self.neighbors(dst)) neighbors = (src_nbrs & dst_nbrs) - set([src, dst]) + n = self.number_of_nodes() if not in_place: # Randomly select an id - new = random.randint(1, 99999999) + new = random.randint(1, 2*n) while new in self: - new = random.randint(1, 99999999) + new = random.randint(1, 2*n) self.add_node(new) for neighbor in neighbors: From ac6fbaa4ef4b6e0645546fa3adc55b90ea3d9729 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 4 Oct 2014 15:24:01 -0500 Subject: [PATCH 0441/1122] Allow for unicode version info --- setup.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index 4058b602..56f0039d 100755 --- a/setup.py +++ b/setup.py @@ -82,14 +82,19 @@ version='%s' def get_package_version(package): version = [] for version_attr in ('version', 'VERSION', '__version__'): - if hasattr(package, version_attr) \ - and isinstance(getattr(package, version_attr), str): - version_info = getattr(package, version_attr, '') - for part in re.split('\D+', version_info): - try: - version.append(int(part)) - except ValueError: - pass + version_info = getattr(package, version_attr, None) + try: + parts = re.split('\D+', version_info) + except TypeError: + continue + for part in parts: + try: + version.append(int(part)) + except ValueError: + pass + if not version: + print(package) + return tuple(version) @@ -112,6 +117,7 @@ def check_requirements(): dep_error = True if dep_error: + print('***********', package_version) raise ImportError('You need `%s` version %d.%d or later.' \ % ((package_name, ) + min_version)) From 0d29ded91452d25ea3ed076b2164c656899fde80 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 4 Oct 2014 15:58:27 -0500 Subject: [PATCH 0442/1122] Try a newer mpl due to Tkinter err on install --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 62d55886..594145d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,8 +49,8 @@ before_install: - travis_retry pip install wheel; - if [[ $ENV != python=2.6* ]]; then travis_retry pip install --no-index --find-links=http://wheels.scikit-image.org/ matplotlib; - else - pip install matplotlib==1.0.1; + else + pip install matplotlib==1.1.0; fi - python check_bento_build.py From 124db3274fb9099bc1f15521c9a92d5fc6773611 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 4 Oct 2014 15:58:28 -0500 Subject: [PATCH 0443/1122] Fix error in viewer test when mpl is present but not qt. --- skimage/viewer/utils/core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index 19a5c73d..90521f9c 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -15,6 +15,9 @@ try: if 'agg' not in mpl.get_backend().lower(): print("Recommended matplotlib backend is `Agg` for full " "skimage.viewer functionality.") + else: + FigureCanvasQTAgg = object + LinearSegmentedColormap = object except ImportError: FigureCanvasQTAgg = object # hack to prevent nosetest and autodoc errors LinearSegmentedColormap = object From 9ea43d777ba351b1c6aaf89d65a2185c1c60c6b2 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 4 Oct 2014 21:41:35 -0500 Subject: [PATCH 0444/1122] Avoid type incompatibility warnings --- skimage/draw/draw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/draw/draw.py b/skimage/draw/draw.py index 2f03b018..01fb4bf3 100644 --- a/skimage/draw/draw.py +++ b/skimage/draw/draw.py @@ -63,7 +63,7 @@ def ellipse(cy, cx, yradius, xradius, shape=None): return _ellipse_in_shape(shape, center, radiuses) else: # rounding here is necessary to avoid rounding issues later - upper_left = np.floor(center - radiuses) + upper_left = np.floor(center - radiuses).astype(int) shifted_center = center - upper_left From 8e318d070f696daca4d12b1c926d9dbd11938c10 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 4 Oct 2014 22:09:21 -0500 Subject: [PATCH 0445/1122] Add networkx to requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 9bb56a8a..2180d724 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ matplotlib>=1.0 numpy>=1.6 scipy>=0.9 six>=1.3 +networkx>=1.6.2 From d844e437792726f85c123c929695e84055e91491 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 4 Oct 2014 22:09:49 -0500 Subject: [PATCH 0446/1122] Update depends and add simpleitk and imread --- DEPENDS.txt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/DEPENDS.txt b/DEPENDS.txt index 25bdf700..79bda5ed 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -12,7 +12,8 @@ You can use pip to automatically install the base dependencies as follows:: Runtime requirements -------------------- * `SciPy >= 0.9 `__ -* `Matplotlib >= 1.0 `__ +* `Matplotlib >= 1.0 `__ +* `NetworkX >= 1.8 ` Known build errors ------------------ @@ -29,9 +30,6 @@ Optional Requirements You can use this scikit with the basic requirements listed above, but some functionality is only available with the following installed: -* `NetworkX >= 1.8 `__ is needed to use region -adjacency graph (RAG)-based segmentation functions. - * `PyQt4 `__ The ``qt`` plugin that provides ``imshow(x, fancy=True)`` and `skivi`. @@ -47,7 +45,15 @@ adjacency graph (RAG)-based segmentation functions. (or `PIL `__) The ``Pillow`` library (or equivalently ``PIL``) is used for Input/Output. -* `Astropy `__ is required to use the FITS io plug-in. +* `Astropy `__ provides FITS io capability. + +*`SimpleITK ` + Optional io plugin providing a wide variety of `formats `__. + including specialized formats using in Medical imagery. + +*`imread ` + Optional io plugin providing most standard `formats `__. + Testing requirements -------------------- From 92c28f97a1fc650414a02ee1b7430ad8df25ba3e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 4 Oct 2014 22:10:04 -0500 Subject: [PATCH 0447/1122] Update travis to better reflect requirements --- .travis.yml | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 594145d2..a615a96c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -57,6 +57,12 @@ before_install: install: - tools/header.py "Dependency versions" - tools/build_versions.py + # Matplotlib settings + - export MPL_DIR=$HOME/.config/matplotlib + - mkdir -p $MPL_DIR + - touch $MPL_DIR/matplotlibrc + - "echo 'backend : Agg' > $MPL_DIR/matplotlibrc" + - "echo 'backend.qt4 : PyQt4' >> $MPL_DIR/matplotlibrc" - export PYTHONWARNINGS=all - python setup.py build_ext --inplace @@ -73,22 +79,19 @@ script: # Install optional dependencies to get full test coverage # Notes: - # - pyfits and imread do NOT support py3.2 + # - imread does NOT support py3.2 # - Use the png headers included in anaconda (from matplotlib install) # TODO: Remove the libm removal when anaconda fixes their libraries # The solution was suggested here # https://groups.google.com/a/continuum.io/forum/#!topic/anaconda/-DLG2ZdTkw0 - if [[ $ENV == python=3.2 ]]; then travis_retry sudo apt-get install python3-pyqt4; - travis_retry pip install networkx; else - travis_retry conda install networkx; sudo cp ~/miniconda/envs/test/include/png* /usr/include; rm ~/miniconda/envs/test/lib/libm-2.5.so; rm ~/miniconda/envs/test/lib/libm.*; sudo apt-get install libtiff4-dev libwebp-dev xcftools; travis_retry pip install imread; - travis_retry pip install pyfits; travis_retry pip install pillow; fi - sudo apt-get install libfreeimage3 @@ -96,13 +99,8 @@ script: - if [[ $ENV != python=3.4* ]]; then travis_retry easy_install SimpleITK; fi - - # Matplotlib settings - - export MPL_DIR=$HOME/.config/matplotlib - - mkdir -p $MPL_DIR - - touch $MPL_DIR/matplotlibrc - - "echo 'backend : Agg' > $MPL_DIR/matplotlibrc" - - "echo 'backend.qt4 : PyQt4' >> $MPL_DIR/matplotlibrc" + - travis_retry pip install pyamg + - travis_retry pip install --no-deps astropy - echo -e $SPACER From 1ce325008abec0c0abaa37eceb54ef7297877739 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 4 Oct 2014 22:33:44 -0500 Subject: [PATCH 0448/1122] Allow for 3-part dependency versions --- requirements.txt | 4 ++-- setup.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2180d724..5ddda45f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -cython>=0.19 -matplotlib>=1.0 +cython>=0.19.2 +matplotlib>=1.1 numpy>=1.6 scipy>=0.9 six>=1.3 diff --git a/setup.py b/setup.py index 56f0039d..ffbb7e6f 100755 --- a/setup.py +++ b/setup.py @@ -117,9 +117,8 @@ def check_requirements(): dep_error = True if dep_error: - print('***********', package_version) raise ImportError('You need `%s` version %d.%d or later.' \ - % ((package_name, ) + min_version)) + % ((package_name, ) + min_version[:2])) if __name__ == "__main__": From 3d4a0812818d2fffe820c23d19f013644dc8dd0e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 4 Oct 2014 22:36:23 -0500 Subject: [PATCH 0449/1122] Print full required version str --- setup.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index ffbb7e6f..3d71f3f4 100755 --- a/setup.py +++ b/setup.py @@ -92,8 +92,6 @@ def get_package_version(package): version.append(int(part)) except ValueError: pass - if not version: - print(package) return tuple(version) @@ -117,8 +115,8 @@ def check_requirements(): dep_error = True if dep_error: - raise ImportError('You need `%s` version %d.%d or later.' \ - % ((package_name, ) + min_version[:2])) + raise ImportError('You need `%s` version %s or later.' \ + % (package_name, '.'.join(str(i) for i in min_version))) if __name__ == "__main__": From b4b7760a9c7c4b566d26d80e91f50918594900ba Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 4 Oct 2014 22:57:43 -0500 Subject: [PATCH 0450/1122] Install networkx up front --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a615a96c..f187bb57 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,7 +34,7 @@ before_install: sudo apt-get install python3-numpy; wget https://raw.githubusercontent.com/numpy/numpy/master/numpy/_import_tools.py -O /home/travis/virtualenv/python3.2_with_system_site_packages/lib/python3.2/site-packages/numpy/_import_tools.py; sudo apt-get install python3-scipy; - travis_retry pip install cython flake8 six; + travis_retry pip install cython flake8 six networkx; else wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; bash miniconda.sh -b -p $HOME/miniconda; @@ -43,7 +43,7 @@ before_install: conda config --set always_yes yes; conda update conda; conda info -a; - travis_retry conda create -n test $ENV six scipy pip flake8 nose; + travis_retry conda create -n test $ENV six scipy pip flake8 nose networkx; source activate test; fi - travis_retry pip install wheel; From 4f91a4ded3468a2a5bd3c8f3bb9c7145c5b4ea2a Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sun, 5 Oct 2014 17:03:56 +0530 Subject: [PATCH 0451/1122] cleanup of if else block --- skimage/graph/rag.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 5820250c..a300c486 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -79,7 +79,7 @@ class RAG(nx.Graph): **extra_keywords)`. `src`, `dst` and `n` are IDs of vertices in the RAG object which is in turn a subclass of `networkx.Graph`. - in_place : bool + in_place : bool, optional If set to `True`, the merged node has the id `dst`, else merged node has a new id which is returned. extra_arguments : sequence, optional @@ -95,30 +95,26 @@ class RAG(nx.Graph): """ src_nbrs = set(self.neighbors(src)) dst_nbrs = set(self.neighbors(dst)) - neighbors = (src_nbrs & dst_nbrs) - set([src, dst]) - n = self.number_of_nodes() - if not in_place: - # Randomly select an id + neighbors = (src_nbrs | dst_nbrs) - set([src, dst]) + + if in_place: + new = dst + else: + n = self.number_of_nodes() new = random.randint(1, 2*n) while new in self: new = random.randint(1, 2*n) self.add_node(new) for neighbor in neighbors: - w = weight_func(self, src, dst, neighbor, *extra_arguments, + w = weight_func(self, src, new, neighbor, *extra_arguments, **extra_keywords) - if in_place: - self.add_edge(neighbor, dst, weight=w) - else: - self.add_edge(neighbor, new, weight=w) + self.add_edge(neighbor, new, weight=w) - if in_place: - self.node[dst]['labels'] += self.node[src]['labels'] - self.remove_node(src) - else: - self.node[new]['labels'] = (self.node[src]['labels'] + - self.node[dst]['labels']) - self.remove_node(src) + self.node[new]['labels'] = (self.node[src]['labels'] + + self.node[dst]['labels']) + self.remove_node(src) + if not in_place: self.remove_node(dst) return new From e6270c52b671156958e03479a436056520ef1ba3 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 5 Oct 2014 07:07:19 -0500 Subject: [PATCH 0452/1122] Do not require jpg support from io plugins in novice test --- skimage/novice/tests/test_novice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/novice/tests/test_novice.py b/skimage/novice/tests/test_novice.py index a1f750ab..02d5c7a8 100644 --- a/skimage/novice/tests/test_novice.py +++ b/skimage/novice/tests/test_novice.py @@ -143,14 +143,14 @@ def test_update_on_save(): assert pic.modified assert pic.path is None - fd, filename = tempfile.mkstemp(suffix=".jpg") + fd, filename = tempfile.mkstemp(suffix=".png") os.close(fd) try: pic.save(filename) assert not pic.modified assert_equal(pic.path, os.path.abspath(filename)) - assert_equal(pic.format, "jpeg") + assert_equal(pic.format, "png") finally: os.unlink(filename) From ec55adf4ea04e5af5067ec623f325af02de9c3c9 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 5 Oct 2014 10:03:34 -0500 Subject: [PATCH 0453/1122] Add pillow to requirements (but allow for PIL) --- requirements.txt | 1 + setup.py | 6 ++++-- skimage/novice/tests/test_novice.py | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5ddda45f..13a6f59b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ numpy>=1.6 scipy>=0.9 six>=1.3 networkx>=1.6.2 +pillow>=1.1.7 diff --git a/setup.py b/setup.py index 3d71f3f4..83979b1a 100755 --- a/setup.py +++ b/setup.py @@ -80,7 +80,6 @@ version='%s' def get_package_version(package): - version = [] for version_attr in ('version', 'VERSION', '__version__'): version_info = getattr(package, version_attr, None) try: @@ -105,8 +104,11 @@ def check_requirements(): if package_name == 'cython': package_name = 'Cython' dep_error = False + if package_name.lower() == 'pillow': + package_name = 'PIL.Image' try: - package = __import__(package_name) + package = __import__(package_name, + fromlist=[package_name.split('.')[-1]]) except ImportError: dep_error = True else: diff --git a/skimage/novice/tests/test_novice.py b/skimage/novice/tests/test_novice.py index 02d5c7a8..a1f750ab 100644 --- a/skimage/novice/tests/test_novice.py +++ b/skimage/novice/tests/test_novice.py @@ -143,14 +143,14 @@ def test_update_on_save(): assert pic.modified assert pic.path is None - fd, filename = tempfile.mkstemp(suffix=".png") + fd, filename = tempfile.mkstemp(suffix=".jpg") os.close(fd) try: pic.save(filename) assert not pic.modified assert_equal(pic.path, os.path.abspath(filename)) - assert_equal(pic.format, "png") + assert_equal(pic.format, "jpeg") finally: os.unlink(filename) From 4a41febde45c5be5992213a507f0dca452925db8 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 5 Oct 2014 10:05:17 -0500 Subject: [PATCH 0454/1122] Move Pillow to runtime reqs in DEPENDS --- DEPENDS.txt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/DEPENDS.txt b/DEPENDS.txt index 79bda5ed..3711796c 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -13,7 +13,9 @@ Runtime requirements -------------------- * `SciPy >= 0.9 `__ * `Matplotlib >= 1.0 `__ -* `NetworkX >= 1.8 ` +* `NetworkX >= 1.8 `__ +* `Pillow `__ + (or `PIL `__) Known build errors ------------------ @@ -41,10 +43,6 @@ functionality is only available with the following installed: The ``pyamg`` module is used for the fast `cg_mg` mode of random walker segmentation. -* `Pillow `__ - (or `PIL `__) - The ``Pillow`` library (or equivalently ``PIL``) is used for Input/Output. - * `Astropy `__ provides FITS io capability. *`SimpleITK ` From 99c95f8e4f7dfd4a9f3805d93f565bfa3ee60018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 5 Oct 2014 11:19:37 -0400 Subject: [PATCH 0455/1122] Refer to rank filters from morphology functions --- skimage/morphology/grey.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 19a4a346..4dfdbc3d 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -35,6 +35,11 @@ def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): eroded : uint8 array The result of the morphological erosion. + Notes + ----- + The lower algorithm complexity makes the `skimage.filter.rank.minimum` + more efficient for larger images and structuring elements. + Examples -------- >>> # Erosion shrinks bright regions @@ -90,6 +95,11 @@ def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): dilated : uint8 array The result of the morphological dilation. + Notes + ----- + The lower algorithm complexity makes the `skimage.filter.rank.maximum` + more efficient for larger images and structuring elements. + Examples -------- >>> # Dilation enlarges bright regions From 1ae929224e5b7520c8ddbce4888cb878cdb22dcb Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 5 Oct 2014 10:31:18 -0500 Subject: [PATCH 0456/1122] Fix setup version requirement handling --- setup.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 83979b1a..838c7410 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ for line in data.splitlines(): version.append(int(part)) except ValueError: pass - DEPENDENCIES[pkg.lower()] = tuple(version) + DEPENDENCIES[str(pkg.lower())] = tuple(version) def configuration(parent_package='', top_path=None): @@ -86,20 +86,19 @@ def get_package_version(package): parts = re.split('\D+', version_info) except TypeError: continue + version = [] for part in parts: try: version.append(int(part)) except ValueError: pass - return tuple(version) - + return tuple(version) def check_requirements(): if sys.version_info < PYTHON_VERSION: raise SystemExit('You need Python version %d.%d or later.' \ % PYTHON_VERSION) - for package_name, min_version in DEPENDENCIES.items(): if package_name == 'cython': package_name = 'Cython' @@ -115,7 +114,6 @@ def check_requirements(): package_version = get_package_version(package) if min_version > package_version: dep_error = True - if dep_error: raise ImportError('You need `%s` version %s or later.' \ % (package_name, '.'.join(str(i) for i in min_version))) From 558dfad3f0456b0e14474aab397d0e43df1ca077 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 5 Oct 2014 13:07:35 -0500 Subject: [PATCH 0457/1122] Fix __import__ fromlist --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 838c7410..856a9710 100755 --- a/setup.py +++ b/setup.py @@ -107,7 +107,7 @@ def check_requirements(): package_name = 'PIL.Image' try: package = __import__(package_name, - fromlist=[package_name.split('.')[-1]]) + fromlist=[package_name.rpartition('.')[0]]) except ImportError: dep_error = True else: From 71c51a42392b5cbc4a7d289f1565b856ae386f63 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 5 Oct 2014 13:31:08 -0500 Subject: [PATCH 0458/1122] Add the rest of the packages to the build_versions script --- tools/build_versions.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tools/build_versions.py b/tools/build_versions.py index f598866e..af1faac2 100755 --- a/tools/build_versions.py +++ b/tools/build_versions.py @@ -6,7 +6,14 @@ import numpy as np import scipy as sp import matplotlib as mpl import six +from PIL import Image +import Cython +import networkx -for m in (np, sp, mpl, six): - if not m is None: - print(m.__name__.rjust(10), ' ', m.__version__) + +for m in (np, sp, mpl, six, Image, networkx, Cython): + if m is Image: + version = m.VERSION + else: + version = m.__version__ + print(m.__name__.rjust(10), ' ', version) From 99f5fd8f6ee7e5fe91e6841c2dad878aca15d6c5 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 5 Oct 2014 15:14:15 -0500 Subject: [PATCH 0459/1122] Use distutils LooseVersion for version check --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 856a9710..2a3cb690 100755 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ import sys import setuptools from distutils.command.build_py import build_py +from distutils.version import LooseVersion # These are manually checked. @@ -112,7 +113,7 @@ def check_requirements(): dep_error = True else: package_version = get_package_version(package) - if min_version > package_version: + if LooseVersion(min_version) > LooseVersion(package_version): dep_error = True if dep_error: raise ImportError('You need `%s` version %s or later.' \ From a765e96c114a882569d46852879dd6e63c325d0c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 5 Oct 2014 15:32:35 -0500 Subject: [PATCH 0460/1122] Install pillow from wheelhouse --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f187bb57..fb9d6acc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -48,10 +48,11 @@ before_install: fi - travis_retry pip install wheel; - if [[ $ENV != python=2.6* ]]; then - travis_retry pip install --no-index --find-links=http://wheels.scikit-image.org/ matplotlib; + travis_retry pip install matplotlib --no-index --find-links=http://wheels.scikit-image.org/; else pip install matplotlib==1.1.0; fi + - pip install pillow --no-index --find-links=http://wheels.scikit-image.org/ - python check_bento_build.py install: From 7cb883999cc8de56eaa4d3ff44b05ee018050ccc Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 5 Oct 2014 15:34:33 -0500 Subject: [PATCH 0461/1122] Use strings as required by LooseVersion --- setup.py | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/setup.py b/setup.py index 2a3cb690..a211fb7a 100755 --- a/setup.py +++ b/setup.py @@ -39,13 +39,7 @@ for line in data.splitlines(): # Only require Cython if we have a developer checkout if pkg.lower() == 'cython' and not VERSION.endswith('dev'): continue - version = [] - for part in re.split('\D+', version_info): - try: - version.append(int(part)) - except ValueError: - pass - DEPENDENCIES[str(pkg.lower())] = tuple(version) + DEPENDENCIES[str(pkg.lower())] = str(version_info) def configuration(parent_package='', top_path=None): @@ -83,18 +77,8 @@ version='%s' def get_package_version(package): for version_attr in ('version', 'VERSION', '__version__'): version_info = getattr(package, version_attr, None) - try: - parts = re.split('\D+', version_info) - except TypeError: - continue - version = [] - for part in parts: - try: - version.append(int(part)) - except ValueError: - pass - - return tuple(version) + if version_info: + return str(version_info) def check_requirements(): if sys.version_info < PYTHON_VERSION: From 4b18721c485b3012ceb3889ef308ba118497d26f Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 5 Oct 2014 16:40:32 -0500 Subject: [PATCH 0462/1122] Fix syntax for python3 in setup.py --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a211fb7a..b191d659 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ for line in data.splitlines(): # Only require Cython if we have a developer checkout if pkg.lower() == 'cython' and not VERSION.endswith('dev'): continue - DEPENDENCIES[str(pkg.lower())] = str(version_info) + DEPENDENCIES[str(pkg).lower()] = str(version_info) def configuration(parent_package='', top_path=None): @@ -80,11 +80,12 @@ def get_package_version(package): if version_info: return str(version_info) + def check_requirements(): if sys.version_info < PYTHON_VERSION: raise SystemExit('You need Python version %d.%d or later.' \ % PYTHON_VERSION) - for package_name, min_version in DEPENDENCIES.items(): + for (package_name, min_version) in DEPENDENCIES.items(): if package_name == 'cython': package_name = 'Cython' dep_error = False From 9355ecd02687be9d9c98f127836cf3d6c8ac2cd5 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 5 Oct 2014 21:12:37 -0500 Subject: [PATCH 0463/1122] Turn off doc examples to see if the build passes otherwise --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index fb9d6acc..89b8c78b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -107,8 +107,8 @@ script: # Run all doc examples - export PYTHONPATH=$(pwd):$PYTHONPATH - - for f in doc/examples/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done - - for f in doc/examples/applications/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done + # for f in doc/examples/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done + # for f in doc/examples/applications/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done - echo -e $SPACER From c3ffc5ed5150aebb1de3522167178e5448599220 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 5 Oct 2014 21:48:44 -0500 Subject: [PATCH 0464/1122] Fix handling of version_attr for python3 --- setup.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index b191d659..451ba468 100755 --- a/setup.py +++ b/setup.py @@ -75,9 +75,9 @@ version='%s' def get_package_version(package): - for version_attr in ('version', 'VERSION', '__version__'): + for version_attr in ('__version__', 'VERSION', 'version'): version_info = getattr(package, version_attr, None) - if version_info: + if version_info and str(version_attr) == version_attr: return str(version_info) @@ -98,6 +98,9 @@ def check_requirements(): dep_error = True else: package_version = get_package_version(package) + print(repr(package_version)) + print(repr(min_version)) + if LooseVersion(min_version) > LooseVersion(package_version): dep_error = True if dep_error: From c8e73d87f1bdcdfbf35d890437581ec15b85ca7c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 5 Oct 2014 22:30:19 -0500 Subject: [PATCH 0465/1122] Do not change matplotlibrc settings until after pyqt install --- .travis.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 89b8c78b..190d2830 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,12 +58,6 @@ before_install: install: - tools/header.py "Dependency versions" - tools/build_versions.py - # Matplotlib settings - - export MPL_DIR=$HOME/.config/matplotlib - - mkdir -p $MPL_DIR - - touch $MPL_DIR/matplotlibrc - - "echo 'backend : Agg' > $MPL_DIR/matplotlibrc" - - "echo 'backend.qt4 : PyQt4' >> $MPL_DIR/matplotlibrc" - export PYTHONWARNINGS=all - python setup.py build_ext --inplace @@ -103,12 +97,19 @@ script: - travis_retry pip install pyamg - travis_retry pip install --no-deps astropy + # Matplotlib settings - must be updated after PyQt is installed + - export MPL_DIR=$HOME/.config/matplotlib + - mkdir -p $MPL_DIR + - touch $MPL_DIR/matplotlibrc + - "echo 'backend : Agg' > $MPL_DIR/matplotlibrc" + - "echo 'backend.qt4 : PyQt4' >> $MPL_DIR/matplotlibrc" + - echo -e $SPACER # Run all doc examples - export PYTHONPATH=$(pwd):$PYTHONPATH - # for f in doc/examples/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done - # for f in doc/examples/applications/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done + - for f in doc/examples/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done + - for f in doc/examples/applications/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done - echo -e $SPACER From 82176abea05c03ee127b3de4d4670c5301b0fcc0 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 05:20:23 -0500 Subject: [PATCH 0466/1122] Only install pyamg on py2.6 and py2.7 --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 190d2830..81dabf69 100644 --- a/.travis.yml +++ b/.travis.yml @@ -94,7 +94,10 @@ script: - if [[ $ENV != python=3.4* ]]; then travis_retry easy_install SimpleITK; fi - - travis_retry pip install pyamg + # PyAMG is only available on Py2.6 and Py2.7 + - if [[ $ENV == python=2.* ]]; then + travis_retry pip install pyamg; + fi - travis_retry pip install --no-deps astropy # Matplotlib settings - must be updated after PyQt is installed From 34505437031fd76a072cbcbb7d549c010b769c0a Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 05:23:17 -0500 Subject: [PATCH 0467/1122] Reinstate pyqt install. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 81dabf69..746dda9e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -87,7 +87,7 @@ script: rm ~/miniconda/envs/test/lib/libm.*; sudo apt-get install libtiff4-dev libwebp-dev xcftools; travis_retry pip install imread; - travis_retry pip install pillow; + conda install pyqt; fi - sudo apt-get install libfreeimage3 # TODO: update when SimpleITK become available on py34 or hopefully pip @@ -98,7 +98,7 @@ script: - if [[ $ENV == python=2.* ]]; then travis_retry pip install pyamg; fi - - travis_retry pip install --no-deps astropy + - travis_retry pip install --no-deps astropy # Matplotlib settings - must be updated after PyQt is installed - export MPL_DIR=$HOME/.config/matplotlib From 76bbb9f6e75281ef1c52df8f0930139832040d0f Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 05:37:37 -0500 Subject: [PATCH 0468/1122] Try a reinstall of matplotlib with pyqt support --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index 746dda9e..07d59c72 100644 --- a/.travis.yml +++ b/.travis.yml @@ -100,6 +100,14 @@ script: fi - travis_retry pip install --no-deps astropy + # Reinstall matplotlib with pyqt support + - pip uninstall matplotlib + - if [[ $ENV != python=2.6* ]]; then + travis_retry pip install matplotlib --no-index --find-links=http://wheels.scikit-image.org/; + else + pip install matplotlib==1.1.0; + fi + # Matplotlib settings - must be updated after PyQt is installed - export MPL_DIR=$HOME/.config/matplotlib - mkdir -p $MPL_DIR From d6ad6ead01e9a36488289930ffa922e0d8165f0f Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 06:05:09 -0500 Subject: [PATCH 0469/1122] Fix yaml syntax --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 07d59c72..9df842ce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -102,7 +102,7 @@ script: # Reinstall matplotlib with pyqt support - pip uninstall matplotlib - - if [[ $ENV != python=2.6* ]]; then + - if [[ $ENV != python=2.6* ]]; then travis_retry pip install matplotlib --no-index --find-links=http://wheels.scikit-image.org/; else pip install matplotlib==1.1.0; From bc1ce12248e2f9840b876ae63f7a708ef2d85e90 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 06:17:02 -0500 Subject: [PATCH 0470/1122] Revert to mostly working travis --- .travis.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9df842ce..746dda9e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -100,14 +100,6 @@ script: fi - travis_retry pip install --no-deps astropy - # Reinstall matplotlib with pyqt support - - pip uninstall matplotlib - - if [[ $ENV != python=2.6* ]]; then - travis_retry pip install matplotlib --no-index --find-links=http://wheels.scikit-image.org/; - else - pip install matplotlib==1.1.0; - fi - # Matplotlib settings - must be updated after PyQt is installed - export MPL_DIR=$HOME/.config/matplotlib - mkdir -p $MPL_DIR From fdeeaa298a3c647cade38c36280aa59201241df5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 6 Oct 2014 08:29:04 -0400 Subject: [PATCH 0471/1122] Add note about supported data types in rank filter replacements --- skimage/morphology/grey.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 4dfdbc3d..1f1c45d4 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -37,8 +37,9 @@ def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): Notes ----- - The lower algorithm complexity makes the `skimage.filter.rank.minimum` - more efficient for larger images and structuring elements. + For `uint8` and `uint16` data, the lower algorithm complexity makes the + `skimage.filter.rank.minimum` function more efficient for larger images + and structuring elements. Examples -------- @@ -97,8 +98,9 @@ def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): Notes ----- - The lower algorithm complexity makes the `skimage.filter.rank.maximum` - more efficient for larger images and structuring elements. + For `uint8` and `uint16` data, the lower algorithm complexity makes the + `skimage.filter.rank.maximum` function more efficient for larger images + and structuring elements. Examples -------- From 028fb533688bb234821b449488c177c3a863c64f Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 14:43:44 -0500 Subject: [PATCH 0472/1122] Bump matplotlib to 1.2 --- .travis.yml | 2 +- DEPENDS.txt | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 746dda9e..4f1ac94f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,7 +50,7 @@ before_install: - if [[ $ENV != python=2.6* ]]; then travis_retry pip install matplotlib --no-index --find-links=http://wheels.scikit-image.org/; else - pip install matplotlib==1.1.0; + pip install matplotlib==1.2.0; fi - pip install pillow --no-index --find-links=http://wheels.scikit-image.org/ - python check_bento_build.py diff --git a/DEPENDS.txt b/DEPENDS.txt index 3711796c..0003de6b 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -12,7 +12,7 @@ You can use pip to automatically install the base dependencies as follows:: Runtime requirements -------------------- * `SciPy >= 0.9 `__ -* `Matplotlib >= 1.0 `__ +* `Matplotlib >= 1.2 `__ * `NetworkX >= 1.8 `__ * `Pillow `__ (or `PIL `__) diff --git a/requirements.txt b/requirements.txt index 13a6f59b..2b55f91e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ cython>=0.19.2 -matplotlib>=1.1 +matplotlib>=1.2 numpy>=1.6 scipy>=0.9 six>=1.3 From d19a04192965c185974eae5a3ae22a2e92bbb705 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 15:15:39 -0500 Subject: [PATCH 0473/1122] Try matplotlib 1.1.1 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4f1ac94f..c6b88c8f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,7 +50,7 @@ before_install: - if [[ $ENV != python=2.6* ]]; then travis_retry pip install matplotlib --no-index --find-links=http://wheels.scikit-image.org/; else - pip install matplotlib==1.2.0; + pip install matplotlib==1.1.1; fi - pip install pillow --no-index --find-links=http://wheels.scikit-image.org/ - python check_bento_build.py From 69e137be1b7f5987ba2fbd3350e20f7540cce38e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 15:25:17 -0500 Subject: [PATCH 0474/1122] Fix required version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2b55f91e..13a6f59b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ cython>=0.19.2 -matplotlib>=1.2 +matplotlib>=1.1 numpy>=1.6 scipy>=0.9 six>=1.3 From 20155926b5a2f6e0143ab5bdad796d6c58fc86da Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 18:05:31 -0500 Subject: [PATCH 0475/1122] Update to use wheelhouse --- .travis.yml | 118 +++++++++++++++++++---------------------------- requirements.txt | 4 +- 2 files changed, 50 insertions(+), 72 deletions(-) diff --git a/.travis.yml b/.travis.yml index c6b88c8f..f21c53d1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,54 +5,33 @@ language: python - -# Tthe Travis python is set to 3.2 for all builds, since we use the default python for the 3.2 build and the anaconda python otherwise python: + - 2.6 + - "2.7_with_system_site_packages" - 3.2 + - 3.3 + - 3.4 env: - - ENV="python=2.6 numpy=1.6 cython=0.19.2" - - ENV="python=2.7 numpy cython" - - ENV="python=3.2" - - ENV="python=3.3 numpy cython" - - ENV="python=3.4 numpy cython" - -virtualenv: - system_site_packages: true + WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" before_install: - export DISPLAY=:99.0 - export SPACER="\n\n\n\n\n\n\n\n\n\n*************************\n\n" - sh -e /etc/init.d/xvfb start - sudo apt-get update - - sudo apt-get install python-dev libjpeg-dev libfreetype6-dev zlib1g-dev - # Python 3.2 is not supported by Miniconda, so we use the package manager for that run. - # NumPy has a bug in python 3 that is only fixed in the latest version, - # hence the below wget of numpy/_import_tools.py from github. - - if [[ $ENV == python=3.2 ]]; then - sudo apt-get install python3-numpy; - wget https://raw.githubusercontent.com/numpy/numpy/master/numpy/_import_tools.py -O /home/travis/virtualenv/python3.2_with_system_site_packages/lib/python3.2/site-packages/numpy/_import_tools.py; - sudo apt-get install python3-scipy; - travis_retry pip install cython flake8 six networkx; - else - wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; - bash miniconda.sh -b -p $HOME/miniconda; - export PATH="$HOME/miniconda/bin:$PATH"; - hash -r; - conda config --set always_yes yes; - conda update conda; - conda info -a; - travis_retry conda create -n test $ENV six scipy pip flake8 nose networkx; - source activate test; - fi - - travis_retry pip install wheel; - - if [[ $ENV != python=2.6* ]]; then - travis_retry pip install matplotlib --no-index --find-links=http://wheels.scikit-image.org/; - else - pip install matplotlib==1.1.1; - fi - - pip install pillow --no-index --find-links=http://wheels.scikit-image.org/ + - travis_retry pip install wheel flake8 coveralls nose six + # on Python 2.7, use the system versions of numpy, scipy, and matplotlib + - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then + travis_retry sudo apt-get install python-scipy python-matplotlib; + travis_retry sudo apt-get install libtiff4-dev libjpeg8-dev zlib1g-dev \ + libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev \ + python-dev python-tk + travis_retry pip install pillow cython $WHEELHOUSE; + else + travis_retry pip install -r requirements.txt $WHEELHOUSE; + fi - python check_bento_build.py install: @@ -72,40 +51,41 @@ script: - echo -e $SPACER - # Install optional dependencies to get full test coverage - # Notes: - # - imread does NOT support py3.2 - # - Use the png headers included in anaconda (from matplotlib install) - # TODO: Remove the libm removal when anaconda fixes their libraries - # The solution was suggested here - # https://groups.google.com/a/continuum.io/forum/#!topic/anaconda/-DLG2ZdTkw0 - - if [[ $ENV == python=3.2 ]]; then - travis_retry sudo apt-get install python3-pyqt4; - else - sudo cp ~/miniconda/envs/test/include/png* /usr/include; - rm ~/miniconda/envs/test/lib/libm-2.5.so; - rm ~/miniconda/envs/test/lib/libm.*; - sudo apt-get install libtiff4-dev libwebp-dev xcftools; - travis_retry pip install imread; - conda install pyqt; - fi - - sudo apt-get install libfreeimage3 - # TODO: update when SimpleITK become available on py34 or hopefully pip - - if [[ $ENV != python=3.4* ]]; then - travis_retry easy_install SimpleITK; - fi - # PyAMG is only available on Py2.6 and Py2.7 - - if [[ $ENV == python=2.* ]]; then - travis_retry pip install pyamg; - fi - - travis_retry pip install --no-deps astropy + # Install Qt and then update the Matplotlib settings + - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then + sudo apt-get install python-qt4; + else + sudo apt-get install libqt4-dev; + travis_retry pip install PySide $WHEELHOUSE; + python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install; + fi - # Matplotlib settings - must be updated after PyQt is installed + # Matplotlib settings - must be after we install Pyside - export MPL_DIR=$HOME/.config/matplotlib - mkdir -p $MPL_DIR - touch $MPL_DIR/matplotlibrc - "echo 'backend : Agg' > $MPL_DIR/matplotlibrc" - - "echo 'backend.qt4 : PyQt4' >> $MPL_DIR/matplotlibrc" + + - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then + "echo 'backend.qt4 : PyQt4' >> $MPL_DIR/matplotlibrc"; + else + "echo 'backend.qt4 : PySide' >> $MPL_DIR/matplotlibrc"; + fi + + # - imread does NOT support py3.2 + - if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then + sudo apt-get install libtiff4-dev libwebp-dev xcftools; + travis_retry pip install imread; + fi + # TODO: update when SimpleITK become available on py34 or hopefully pip + - if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then + travis_retry easy_install SimpleITK; + fi + - travis_retry pip install --no-deps astropy + + - if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then + travis_retry pip install pyamg; + fi - echo -e $SPACER @@ -118,13 +98,11 @@ script: # run tests again with optional dependencies to get more coverage # measure coverage on py3.3 - - if [[ $ENV == python=3.3* ]]; then + - if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then nosetests --exe -v --with-doctest --with-cov --cover-package skimage; else nosetests --exe -v --with-doctest skimage; fi after_success: - - if [[ $ENV == python=3.3* ]]; then - coveralls; - fi + - coveralls; diff --git a/requirements.txt b/requirements.txt index 13a6f59b..7464d13d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ cython>=0.19.2 -matplotlib>=1.1 -numpy>=1.6 +matplotlib>=1.1.1 +numpy>=1.6.1 scipy>=0.9 six>=1.3 networkx>=1.6.2 From 85711410b056291742853e70417931fd6ebf83ee Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 18:19:19 -0500 Subject: [PATCH 0476/1122] Fix travis syntax and use tools/header.py --- .travis.yml | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index f21c53d1..7de2255d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,6 @@ env: before_install: - export DISPLAY=:99.0 - - export SPACER="\n\n\n\n\n\n\n\n\n\n*************************\n\n" - sh -e /etc/init.d/xvfb start - sudo apt-get update @@ -44,21 +43,21 @@ script: # Run all tests with minimum dependencies - nosetests --exe -v skimage - - echo -e $SPACER + - tools/header.py "Pep8 and Flake tests" # Run pep8 and flake tests - flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples - - echo -e $SPACER + - tools/header.py "Install optional dependencies" # Install Qt and then update the Matplotlib settings - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then sudo apt-get install python-qt4; - else - sudo apt-get install libqt4-dev; - travis_retry pip install PySide $WHEELHOUSE; - python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install; - fi + else + sudo apt-get install libqt4-dev; + travis_retry pip install PySide $WHEELHOUSE; + python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install; + fi # Matplotlib settings - must be after we install Pyside - export MPL_DIR=$HOME/.config/matplotlib @@ -66,11 +65,11 @@ script: - touch $MPL_DIR/matplotlibrc - "echo 'backend : Agg' > $MPL_DIR/matplotlibrc" - - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then - "echo 'backend.qt4 : PyQt4' >> $MPL_DIR/matplotlibrc"; - else - "echo 'backend.qt4 : PySide' >> $MPL_DIR/matplotlibrc"; - fi + - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then + echo 'backend.qt4 : PyQt4' >> $MPL_DIR/matplotlibrc; + else + echo 'backend.qt4 : PySide' >> $MPL_DIR/matplotlibrc; + fi # - imread does NOT support py3.2 - if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then @@ -87,14 +86,14 @@ script: travis_retry pip install pyamg; fi - - echo -e $SPACER + - tools/header.py "Run doc examples" # Run all doc examples - export PYTHONPATH=$(pwd):$PYTHONPATH - for f in doc/examples/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done - for f in doc/examples/applications/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done - - echo -e $SPACER + - tools/header.py "Run tests with all dependencies" # run tests again with optional dependencies to get more coverage # measure coverage on py3.3 From 0c288dee3d71baa4e90f03f9900cec659dd87300 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 18:25:57 -0500 Subject: [PATCH 0477/1122] Clean up travis --- .travis.yml | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7de2255d..8f9ba45f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,17 +20,14 @@ before_install: - sh -e /etc/init.d/xvfb start - sudo apt-get update - - travis_retry pip install wheel flake8 coveralls nose six + - travis_retry pip install wheel flake8 coveralls nose + # on Python 2.7, use the system versions of numpy, scipy, and matplotlib - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then travis_retry sudo apt-get install python-scipy python-matplotlib; - travis_retry sudo apt-get install libtiff4-dev libjpeg8-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev \ - python-dev python-tk - travis_retry pip install pillow cython $WHEELHOUSE; - else - travis_retry pip install -r requirements.txt $WHEELHOUSE; - fi + fi + + - travis_retry pip install -r requirements.txt $WHEELHOUSE; - python check_bento_build.py install: @@ -40,23 +37,24 @@ install: - python setup.py build_ext --inplace script: - # Run all tests with minimum dependencies + - tools/header.py "Run all tests with minimum dependencies" - nosetests --exe -v skimage - tools/header.py "Pep8 and Flake tests" - - # Run pep8 and flake tests - flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples - - tools/header.py "Install optional dependencies" + - tools/header.py "Install optional dependencies" # Install Qt and then update the Matplotlib settings - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then - sudo apt-get install python-qt4; + sudo apt-get install -q python-qt4; + export QT_API=PyQt4; + else - sudo apt-get install libqt4-dev; + sudo apt-get install -q libqt4-dev; travis_retry pip install PySide $WHEELHOUSE; python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install; + export QT_API=Pyside; fi # Matplotlib settings - must be after we install Pyside @@ -64,22 +62,19 @@ script: - mkdir -p $MPL_DIR - touch $MPL_DIR/matplotlibrc - "echo 'backend : Agg' > $MPL_DIR/matplotlibrc" - - - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then - echo 'backend.qt4 : PyQt4' >> $MPL_DIR/matplotlibrc; - else - echo 'backend.qt4 : PySide' >> $MPL_DIR/matplotlibrc; - fi + - "echo 'backend.qt4 : $(QT_API)' >> $MPL_DIR/matplotlibrc" # - imread does NOT support py3.2 - if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then - sudo apt-get install libtiff4-dev libwebp-dev xcftools; + sudo apt-get install -q libtiff4-dev libwebp-dev xcftools; travis_retry pip install imread; fi + # TODO: update when SimpleITK become available on py34 or hopefully pip - if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then travis_retry easy_install SimpleITK; fi + - travis_retry pip install --no-deps astropy - if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then @@ -87,14 +82,11 @@ script: fi - tools/header.py "Run doc examples" - - # Run all doc examples - export PYTHONPATH=$(pwd):$PYTHONPATH - for f in doc/examples/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done - for f in doc/examples/applications/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done - tools/header.py "Run tests with all dependencies" - # run tests again with optional dependencies to get more coverage # measure coverage on py3.3 - if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then From 967b1912c23d60441760f53954a772377bd4b958 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 18:30:39 -0500 Subject: [PATCH 0478/1122] Remove print statements --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index 451ba468..f8169691 100755 --- a/setup.py +++ b/setup.py @@ -98,8 +98,6 @@ def check_requirements(): dep_error = True else: package_version = get_package_version(package) - print(repr(package_version)) - print(repr(min_version)) if LooseVersion(min_version) > LooseVersion(package_version): dep_error = True From 0be4a38260dba6f65a858cc5297816439e8aeb87 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 18:47:56 -0500 Subject: [PATCH 0479/1122] Use all min reqs on Python2.7, fix syntax error and variable overshadow --- .travis.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8f9ba45f..c6c4b5a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,8 +23,10 @@ before_install: - travis_retry pip install wheel flake8 coveralls nose # on Python 2.7, use the system versions of numpy, scipy, and matplotlib + # and the minimum version of the rest of the requirements - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then travis_retry sudo apt-get install python-scipy python-matplotlib; + pip install cython==0.19.2 networkx==1.6.2 six==1.3; fi - travis_retry pip install -r requirements.txt $WHEELHOUSE; @@ -48,13 +50,13 @@ script: # Install Qt and then update the Matplotlib settings - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then sudo apt-get install -q python-qt4; - export QT_API=PyQt4; + export SCI_QT_API=PyQt4; else sudo apt-get install -q libqt4-dev; travis_retry pip install PySide $WHEELHOUSE; python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install; - export QT_API=Pyside; + export SCI_QT_API=Pyside; fi # Matplotlib settings - must be after we install Pyside @@ -62,7 +64,7 @@ script: - mkdir -p $MPL_DIR - touch $MPL_DIR/matplotlibrc - "echo 'backend : Agg' > $MPL_DIR/matplotlibrc" - - "echo 'backend.qt4 : $(QT_API)' >> $MPL_DIR/matplotlibrc" + - "echo 'backend.qt4 : $(SCI_QT_API)' >> $MPL_DIR/matplotlibrc" # - imread does NOT support py3.2 - if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then @@ -77,7 +79,7 @@ script: - travis_retry pip install --no-deps astropy - - if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then + - if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then travis_retry pip install pyamg; fi From 4645dc8d9909e1d0e991951d4a7e183f4ef640fd Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 18:59:43 -0500 Subject: [PATCH 0480/1122] Use github downloads for specific versions on Py2.7 --- .travis.yml | 5 +++-- requirements.txt | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index c6c4b5a1..13f93387 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,10 +23,11 @@ before_install: - travis_retry pip install wheel flake8 coveralls nose # on Python 2.7, use the system versions of numpy, scipy, and matplotlib - # and the minimum version of the rest of the requirements + # and the minimum version of cython and networkx - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then travis_retry sudo apt-get install python-scipy python-matplotlib; - pip install cython==0.19.2 networkx==1.6.2 six==1.3; + pip install https://github.com/cython/cython/archive/0.19.2.tar.gz + pip install https://github.com/networkx/networkx/archive/networkx-1.6.tar.gz fi - travis_retry pip install -r requirements.txt $WHEELHOUSE; diff --git a/requirements.txt b/requirements.txt index 7464d13d..6c842e53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,5 @@ matplotlib>=1.1.1 numpy>=1.6.1 scipy>=0.9 six>=1.3 -networkx>=1.6.2 +networkx>=1.6 pillow>=1.1.7 From bdb59ce505179b15d192d28d814ed2371c5e4465 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 19:05:30 -0500 Subject: [PATCH 0481/1122] Fix travis syntax errors --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 13f93387..31a19e67 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,8 +26,8 @@ before_install: # and the minimum version of cython and networkx - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then travis_retry sudo apt-get install python-scipy python-matplotlib; - pip install https://github.com/cython/cython/archive/0.19.2.tar.gz - pip install https://github.com/networkx/networkx/archive/networkx-1.6.tar.gz + pip install https://github.com/cython/cython/archive/0.19.2.tar.gz; + pip install https://github.com/networkx/networkx/archive/networkx-1.6.tar.gz; fi - travis_retry pip install -r requirements.txt $WHEELHOUSE; From cad3f96b2661a33c0544b40c339c82481f94ecc3 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 19:20:45 -0500 Subject: [PATCH 0482/1122] Fix syntax error and QT API var handling. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 31a19e67..c90b7619 100644 --- a/.travis.yml +++ b/.travis.yml @@ -65,7 +65,7 @@ script: - mkdir -p $MPL_DIR - touch $MPL_DIR/matplotlibrc - "echo 'backend : Agg' > $MPL_DIR/matplotlibrc" - - "echo 'backend.qt4 : $(SCI_QT_API)' >> $MPL_DIR/matplotlibrc" + - "echo 'backend.qt4 : $SCI_QT_API' >> $MPL_DIR/matplotlibrc" # - imread does NOT support py3.2 - if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then @@ -78,7 +78,7 @@ script: travis_retry easy_install SimpleITK; fi - - travis_retry pip install --no-deps astropy + - travis_retry pip install --no-deps astropy - if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then travis_retry pip install pyamg; From f2d5435c1960c34e106a432aa815162d4ea918cd Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 20:35:25 -0500 Subject: [PATCH 0483/1122] Fix string interpolation error --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c90b7619..6608ddad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -64,8 +64,8 @@ script: - export MPL_DIR=$HOME/.config/matplotlib - mkdir -p $MPL_DIR - touch $MPL_DIR/matplotlibrc - - "echo 'backend : Agg' > $MPL_DIR/matplotlibrc" - - "echo 'backend.qt4 : $SCI_QT_API' >> $MPL_DIR/matplotlibrc" + - echo "backend : Agg" > $MPL_DIR/matplotlibrc + - echo "backend.qt4 : $SCI_QT_API" >> $MPL_DIR/matplotlibrc # - imread does NOT support py3.2 - if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then From 60db60ff9cccfd3f66d4bd92dd35002e7d84c8ea Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 20:55:35 -0500 Subject: [PATCH 0484/1122] Clean up required versions --- .travis.yml | 2 +- DEPENDS.txt | 8 ++++---- requirements.txt | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6608ddad..31627b2f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,7 @@ before_install: - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then travis_retry sudo apt-get install python-scipy python-matplotlib; pip install https://github.com/cython/cython/archive/0.19.2.tar.gz; - pip install https://github.com/networkx/networkx/archive/networkx-1.6.tar.gz; + pip install https://github.com/networkx/networkx/archive/networkx-1.8.tar.gz; fi - travis_retry pip install -r requirements.txt $WHEELHOUSE; diff --git a/DEPENDS.txt b/DEPENDS.txt index 0003de6b..6a6507ff 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -2,7 +2,7 @@ Build Requirements ------------------ * `Python >= 2.6 `__ * `Numpy >= 1.6 `__ -* `Cython >= 0.19 `__ +* `Cython >= 0.19.2 `__ * `Six >=1.3 `__ You can use pip to automatically install the base dependencies as follows:: @@ -11,9 +11,9 @@ You can use pip to automatically install the base dependencies as follows:: Runtime requirements -------------------- -* `SciPy >= 0.9 `__ -* `Matplotlib >= 1.2 `__ -* `NetworkX >= 1.8 `__ +* `SciPy `__ +* `Matplotlib `__ +* `NetworkX `__ * `Pillow `__ (or `PIL `__) diff --git a/requirements.txt b/requirements.txt index 6c842e53..435803b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,5 @@ matplotlib>=1.1.1 numpy>=1.6.1 scipy>=0.9 six>=1.3 -networkx>=1.6 +networkx>=1.8 pillow>=1.1.7 From 98e230aab660d961346645f71bf70d97a7e37b84 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 21:00:10 -0500 Subject: [PATCH 0485/1122] Another attempt at variable substitution --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 31627b2f..cff89939 100644 --- a/.travis.yml +++ b/.travis.yml @@ -64,8 +64,8 @@ script: - export MPL_DIR=$HOME/.config/matplotlib - mkdir -p $MPL_DIR - touch $MPL_DIR/matplotlibrc - - echo "backend : Agg" > $MPL_DIR/matplotlibrc - - echo "backend.qt4 : $SCI_QT_API" >> $MPL_DIR/matplotlibrc + - "echo 'backend : Agg' > $MPL_DIR/matplotlibrc" + - "echo 'backend.qt4 : '$SCI_QT_API\'' >> $MPL_DIR/matplotlibrc" # - imread does NOT support py3.2 - if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then From 75928e87ee8ba9086c749755746dddcff704be61 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 21:01:57 -0500 Subject: [PATCH 0486/1122] Yet another attempt at variable substitution --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cff89939..ed303a5b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -65,7 +65,7 @@ script: - mkdir -p $MPL_DIR - touch $MPL_DIR/matplotlibrc - "echo 'backend : Agg' > $MPL_DIR/matplotlibrc" - - "echo 'backend.qt4 : '$SCI_QT_API\'' >> $MPL_DIR/matplotlibrc" + - "echo 'backend.qt4 : '$SCI_QT_API >> $MPL_DIR/matplotlibrc" # - imread does NOT support py3.2 - if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then From 6ec45274c8501331ef53c99765ead7e67b967104 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 21:45:04 -0500 Subject: [PATCH 0487/1122] Fix installs for libfreeimage3 and astropy --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ed303a5b..e61fd93e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -78,7 +78,8 @@ script: travis_retry easy_install SimpleITK; fi - - travis_retry pip install --no-deps astropy + - travis_retry sudo apt-get install libfreeimage3 + - travis_retry pip install astropy - if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then travis_retry pip install pyamg; From b46276427a6c191b60a855bf0425ebb4e91c0ec3 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 21:46:02 -0500 Subject: [PATCH 0488/1122] Fix install of imread --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e61fd93e..26d487de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -69,7 +69,7 @@ script: # - imread does NOT support py3.2 - if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then - sudo apt-get install -q libtiff4-dev libwebp-dev xcftools; + sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools; travis_retry pip install imread; fi From 2f61421c33083d62d7290594b8e96450d889ed82 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 7 Oct 2014 05:37:21 -0500 Subject: [PATCH 0489/1122] Clean up simpleitk description and imread link --- DEPENDS.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DEPENDS.txt b/DEPENDS.txt index 6a6507ff..aa3cd021 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -47,9 +47,9 @@ functionality is only available with the following installed: *`SimpleITK ` Optional io plugin providing a wide variety of `formats `__. - including specialized formats using in Medical imagery. + including specialized formats using in medical imaging. -*`imread ` +*`imread ` Optional io plugin providing most standard `formats `__. From 39267d103cd9b8f4ba9cf2ebfc9729141b2624e2 Mon Sep 17 00:00:00 2001 From: Pratap Vardhan Date: Tue, 7 Oct 2014 17:55:44 +0530 Subject: [PATCH 0490/1122] CLN: Improve performance of disk and diamond selems --- skimage/morphology/selem.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index 5e0606c0..ae287304 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -85,10 +85,10 @@ def diamond(radius, dtype=np.uint8): The structuring element where elements of the neighborhood are 1 and 0 otherwise. """ - half = radius - (I, J) = np.meshgrid(range(0, radius * 2 + 1), range(0, radius * 2 + 1)) - s = np.abs(I - half) + np.abs(J - half) - return np.array(s <= radius, dtype=dtype) + L = np.arange(0, radius * 2 + 1) + I, J = np.meshgrid(L, L) + return np.array(np.abs(I - radius) + np.abs(J - radius) <= radius, + dtype=dtype) def disk(radius, dtype=np.uint8): @@ -113,11 +113,9 @@ def disk(radius, dtype=np.uint8): The structuring element where elements of the neighborhood are 1 and 0 otherwise. """ - L = np.linspace(-radius, radius, 2 * radius + 1) - (X, Y) = np.meshgrid(L, L) - s = X**2 - s += Y**2 - return np.array(s <= radius * radius, dtype=dtype) + L = np.arange(-radius, radius + 1) + X, Y = np.meshgrid(L, L) + return np.array((X ** 2 + Y ** 2) <= radius ** 2, dtype=np.uint8) def cube(width, dtype=np.uint8): From 12dab41c3988ce6a5a5383b405e1e20c8fe2b1ad Mon Sep 17 00:00:00 2001 From: Pratap Vardhan Date: Tue, 7 Oct 2014 18:05:57 +0530 Subject: [PATCH 0491/1122] CLN: Correct dtype typo --- skimage/morphology/selem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index ae287304..21200e11 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -115,7 +115,7 @@ def disk(radius, dtype=np.uint8): """ L = np.arange(-radius, radius + 1) X, Y = np.meshgrid(L, L) - return np.array((X ** 2 + Y ** 2) <= radius ** 2, dtype=np.uint8) + return np.array((X ** 2 + Y ** 2) <= radius ** 2, dtype=dtype) def cube(width, dtype=np.uint8): From c73d1c98e015f060c9be3b93e985708afe46e8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 7 Oct 2014 08:38:26 -0400 Subject: [PATCH 0492/1122] Clarify that uint16 images are only faster up to a certain bit depth --- skimage/morphology/grey.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 1f1c45d4..6c901b4a 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -8,6 +8,7 @@ from . import cmorph __all__ = ['erosion', 'dilation', 'opening', 'closing', 'white_tophat', 'black_tophat'] + @default_fallback def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): """Return greyscale morphological erosion of an image. @@ -37,9 +38,9 @@ def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): Notes ----- - For `uint8` and `uint16` data, the lower algorithm complexity makes the - `skimage.filter.rank.minimum` function more efficient for larger images - and structuring elements. + For `uint8` (and `uint16` up to a certain bit-depth) data, the lower + algorithm complexity makes the `skimage.filter.rank.minimum` function more + efficient for larger images and structuring elements. Examples -------- @@ -98,9 +99,9 @@ def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): Notes ----- - For `uint8` and `uint16` data, the lower algorithm complexity makes the - `skimage.filter.rank.maximum` function more efficient for larger images - and structuring elements. + For `uint8` (and `uint16` up to a certain bit-depth) data, the lower + algorithm complexity makes the `skimage.filter.rank.minimum` function more + efficient for larger images and structuring elements. Examples -------- From 53c4d93f425b1b68a81f342ee66cfd02d328a0ea Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 7 Oct 2014 07:56:19 -0500 Subject: [PATCH 0493/1122] Add notes about Travis usage --- .travis.yml | 2 ++ doc/travis_notes.txt | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 doc/travis_notes.txt diff --git a/.travis.yml b/.travis.yml index 26d487de..ab2aaf53 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,8 @@ # After changing this file, check it on: # http://lint.travis-ci.org/ +# See docs/travis_notes.txt for some guidelines + language: python python: diff --git a/doc/travis_notes.txt b/doc/travis_notes.txt new file mode 100644 index 00000000..41118669 --- /dev/null +++ b/doc/travis_notes.txt @@ -0,0 +1,20 @@ + + +Travis Notes: + +- Use http://lint.travis-ci.org/ to make sure it is valid yaml. +- Make sure all of your "-" lines start on the same column +- Make sure all of your "if" lines are aligned with the "else" and "fi" lines +- "If" blocks must be on one travis statement and have semicolons at the end of each line: + +``` + - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then + echo "2.7"; + else + echo "Not 2.7"; + end +``` + +- "If" blocks cannot contain comments +- Use `travis_retry` before a command to have it try several times before failing + (useful for installing from third party sources) From 3cecfd8711ea23c2a5103c9d4c0204691a2c1890 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 7 Oct 2014 08:16:56 -0500 Subject: [PATCH 0494/1122] Add notes about and variable substitution --- doc/travis_notes.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/travis_notes.txt b/doc/travis_notes.txt index 41118669..7256b007 100644 --- a/doc/travis_notes.txt +++ b/doc/travis_notes.txt @@ -16,5 +16,10 @@ Travis Notes: ``` - "If" blocks cannot contain comments +- All travis commands are run with `eval` and quotes are taken as literal characters + unless you wrap the whole line in quotes: +`echo "hello : world"` is interpreted as `echo \"hello : world\"` +`"echo 'hello : world'"` is interpreted as `echo 'hello : world'` +`"echo 'hello : '$(MYVAR)'world'"` is interpreted as `echo 'hello : $(MYVAR)world'` - Use `travis_retry` before a command to have it try several times before failing (useful for installing from third party sources) From e384689f0cece59dfdac347568c06615e3b64d0c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 8 Oct 2014 17:53:01 -0500 Subject: [PATCH 0495/1122] Use tabs to line up if blocks --- .travis.yml | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/.travis.yml b/.travis.yml index ab2aaf53..7d0091f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,11 +26,11 @@ before_install: # on Python 2.7, use the system versions of numpy, scipy, and matplotlib # and the minimum version of cython and networkx - - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then + - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then travis_retry sudo apt-get install python-scipy python-matplotlib; pip install https://github.com/cython/cython/archive/0.19.2.tar.gz; pip install https://github.com/networkx/networkx/archive/networkx-1.8.tar.gz; - fi + fi - travis_retry pip install -r requirements.txt $WHEELHOUSE; - python check_bento_build.py @@ -51,16 +51,16 @@ script: - tools/header.py "Install optional dependencies" # Install Qt and then update the Matplotlib settings - - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then - sudo apt-get install -q python-qt4; - export SCI_QT_API=PyQt4; + - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then + sudo apt-get install -q python-qt4; + export SCI_QT_API=PyQt4; - else - sudo apt-get install -q libqt4-dev; - travis_retry pip install PySide $WHEELHOUSE; - python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install; - export SCI_QT_API=Pyside; - fi + else + sudo apt-get install -q libqt4-dev; + travis_retry pip install PySide $WHEELHOUSE; + python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install; + export SCI_QT_API=Pyside; + fi # Matplotlib settings - must be after we install Pyside - export MPL_DIR=$HOME/.config/matplotlib @@ -70,22 +70,22 @@ script: - "echo 'backend.qt4 : '$SCI_QT_API >> $MPL_DIR/matplotlibrc" # - imread does NOT support py3.2 - - if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then - sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools; - travis_retry pip install imread; - fi + - if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then + sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools; + travis_retry pip install imread; + fi # TODO: update when SimpleITK become available on py34 or hopefully pip - - if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then - travis_retry easy_install SimpleITK; - fi + - if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then + travis_retry easy_install SimpleITK; + fi - travis_retry sudo apt-get install libfreeimage3 - travis_retry pip install astropy - - if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then - travis_retry pip install pyamg; - fi + - if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then + travis_retry pip install pyamg; + fi - tools/header.py "Run doc examples" - export PYTHONPATH=$(pwd):$PYTHONPATH @@ -95,11 +95,11 @@ script: - tools/header.py "Run tests with all dependencies" # run tests again with optional dependencies to get more coverage # measure coverage on py3.3 - - if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then - nosetests --exe -v --with-doctest --with-cov --cover-package skimage; - else - nosetests --exe -v --with-doctest skimage; - fi + - if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then + nosetests --exe -v --with-doctest --with-cov --cover-package skimage; + else + nosetests --exe -v --with-doctest skimage; + fi after_success: - coveralls; From 878a1f3381a8070339789ae231c65951b6fed7e2 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 06:00:54 -0500 Subject: [PATCH 0496/1122] Update Travis notes --- doc/travis_notes.txt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/doc/travis_notes.txt b/doc/travis_notes.txt index 7256b007..16481108 100644 --- a/doc/travis_notes.txt +++ b/doc/travis_notes.txt @@ -5,14 +5,15 @@ Travis Notes: - Use http://lint.travis-ci.org/ to make sure it is valid yaml. - Make sure all of your "-" lines start on the same column - Make sure all of your "if" lines are aligned with the "else" and "fi" lines +- Recommend using tab stops (but not tab chars) everywhere for alignment - "If" blocks must be on one travis statement and have semicolons at the end of each line: ``` - - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then - echo "2.7"; - else - echo "Not 2.7"; - end + - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then + echo "2.7"; + else + echo "Not 2.7"; + end ``` - "If" blocks cannot contain comments @@ -21,5 +22,8 @@ Travis Notes: `echo "hello : world"` is interpreted as `echo \"hello : world\"` `"echo 'hello : world'"` is interpreted as `echo 'hello : world'` `"echo 'hello : '$(MYVAR)'world'"` is interpreted as `echo 'hello : $(MYVAR)world'` -- Use `travis_retry` before a command to have it try several times before failing - (useful for installing from third party sources) +- Use `travis_retry` before a command to have it try several times before +failing (useful for installing from third party sources) +- Feel free to cancel a build rather than waiting for it to go to completion +if you have made a change to that branch. +- A VM with 64bit Ubuntu 12.04 is a huge help. From 1ca05d77cb3cbf90c0c7901fc99da6bda7694f9f Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 06:07:54 -0500 Subject: [PATCH 0497/1122] More updates to travis documentation --- .travis.yml | 4 ++-- doc/travis_notes.txt | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7d0091f1..9325de32 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,9 @@ # vim ft=yaml # After changing this file, check it on: -# http://lint.travis-ci.org/ +# http://yaml-online-parser.appspot.com/ -# See docs/travis_notes.txt for some guidelines +# See doc/travis_notes.txt for some guidelines language: python diff --git a/doc/travis_notes.txt b/doc/travis_notes.txt index 16481108..787e7c11 100644 --- a/doc/travis_notes.txt +++ b/doc/travis_notes.txt @@ -1,12 +1,12 @@ - -Travis Notes: - -- Use http://lint.travis-ci.org/ to make sure it is valid yaml. +- Use http://yaml-online-parser.appspot.com/ to make sure it is valid yaml. + http://lint.travis-ci.org/ is recommended elsewhere but does not give helpful + error reports. - Make sure all of your "-" lines start on the same column - Make sure all of your "if" lines are aligned with the "else" and "fi" lines - Recommend using tab stops (but not tab chars) everywhere for alignment -- "If" blocks must be on one travis statement and have semicolons at the end of each line: +- "If" blocks must be on one travis statement and have semicolons at the + end of each line: ``` - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then @@ -17,13 +17,14 @@ Travis Notes: ``` - "If" blocks cannot contain comments -- All travis commands are run with `eval` and quotes are taken as literal characters - unless you wrap the whole line in quotes: +- All travis commands are run with `eval` and quotes are taken as literal + characters unless you wrap the whole line in quotes: `echo "hello : world"` is interpreted as `echo \"hello : world\"` `"echo 'hello : world'"` is interpreted as `echo 'hello : world'` -`"echo 'hello : '$(MYVAR)'world'"` is interpreted as `echo 'hello : $(MYVAR)world'` +`"echo 'hello : '$(MYVAR)'world'"` is interpreted as + `echo 'hello : $(MYVAR)world'` - Use `travis_retry` before a command to have it try several times before failing (useful for installing from third party sources) - Feel free to cancel a build rather than waiting for it to go to completion -if you have made a change to that branch. + if you have made a change to that branch. - A VM with 64bit Ubuntu 12.04 is a huge help. From df1e2061a1b470379b198de1c3a5cb2a1be48417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 9 Oct 2014 16:32:50 -0400 Subject: [PATCH 0498/1122] Use correct rank filter replacement function --- skimage/morphology/grey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 6c901b4a..cd3dcaa8 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -100,7 +100,7 @@ def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): Notes ----- For `uint8` (and `uint16` up to a certain bit-depth) data, the lower - algorithm complexity makes the `skimage.filter.rank.minimum` function more + algorithm complexity makes the `skimage.filter.rank.maximum` function more efficient for larger images and structuring elements. Examples From 5f769cb4bf5c7644132921ebb01be9b7e84040df Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 24 Sep 2014 16:34:50 -0500 Subject: [PATCH 0499/1122] Fix handling of non-uint8 images for PIL imsave. Fix handling of non-uint8 imags for PIL imsave. Update build requirements and documentation Reorganized dependencies in DEPENDS.txt. Fix install_requires string formatting Use img_as_ubyte instead of the previous hack Add to the docstring notes. Add to docstring notes Fix errant changes Add support for tif floating point and 16bit files Handle case where format_str is None Add note about 2D TIFF file support Add handling for uint16 and integer types. --- skimage/io/_plugins/pil_plugin.py | 49 +++++++++++++++++++------------ 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index f45084ce..413647e1 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -11,7 +11,7 @@ except ImportError: "http://pypi.python.org/pypi/PIL/) " "for further instructions.") -from skimage.util import img_as_ubyte +from skimage.util import img_as_ubyte, img_as_uint, img_as_int, img_as_float from six import string_types @@ -109,29 +109,37 @@ def ndarray_to_pil(arr, format_str=None): if arr.shape[2] not in (3, 4): raise ValueError("Invalid number of channels in image array.") - # Image is floating point, assume in [0, 1] - if np.issubdtype(arr.dtype, float): - arr = arr * 255 + if not format_str is None and format_str in ('tiff', 'tif'): + # open with pylibtiff + pass - arr = arr.astype(np.uint8) + if arr.ndim == 3: + arr = img_as_ubyte(arr) + mode_base = mode = {3: 'RGB', 4: 'RGBA'}[arr.shape[2]] - if arr.ndim == 2: - mode = 'L' + elif arr.dtype.kind == 'f': + arr = img_as_uint(arr) + mode = 'I;16' + mode_base = 'I' - elif arr.shape[2] in (3, 4): - mode = {3: 'RGB', 4: 'RGBA'}[arr.shape[2]] - - # Force all integers to bytes + elif arr.max() < 256 and arr.min() >= 0: arr = arr.astype(np.uint8) + mode = 'L' + mode_base = 'L' - try: - img = Image.frombytes(mode, (arr.shape[1], arr.shape[0]), - arr.tostring()) - except AttributeError: - img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), - arr.tostring()) + elif arr.max() < 2**16 and arr.min() >= 0: + arr = arr.astype(np.uint16) + mode = 'I;16' + mode_base = 'I' - return img + else: + arr = img_as_int(arr) + mode = 'I' + mode_base = 'I' + + im = Image.new(mode_base, arr.T.shape) + im.fromstring(arr.tostring(), 'raw', mode) + return im def imsave(fname, arr, format_str=None): @@ -151,7 +159,10 @@ def imsave(fname, arr, format_str=None): Notes ----- - Currently, only 8-bit precision is supported. + Currently, only 8-bit precision is supported for PNG and JPEG. + Images that are not uint8 type will be converted using + `skimage.img_as_ubyte`. + 2D TIFF Files also support unit16 and float32 type images. """ # default to PNG if file-like object From 7374f4ac53d7b17e305df636dedb57a4959c448a Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 5 Oct 2014 06:58:32 -0500 Subject: [PATCH 0500/1122] Add tifffile.py and its license. Add use of tifffile and its license Actually add the tifffile file and LICENSE --- skimage/io/LICENSE.txt | 8 + skimage/io/_plugins/pil_plugin.py | 18 +- skimage/io/_plugins/tifffile.py | 4844 +++++++++++++++++++++++++++++ 3 files changed, 4863 insertions(+), 7 deletions(-) create mode 100644 skimage/io/LICENSE.txt create mode 100644 skimage/io/_plugins/tifffile.py diff --git a/skimage/io/LICENSE.txt b/skimage/io/LICENSE.txt new file mode 100644 index 00000000..16672bcb --- /dev/null +++ b/skimage/io/LICENSE.txt @@ -0,0 +1,8 @@ + +_plugins.tifffile.py + + Copyright (c) 2008-2014, Christoph Gohlke + Copyright (c) 2008-2014, The Regents of the University of California + Produced at the Laboratory for Fluorescence Dynamics + + Distributed under the terms of the BSD License. diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 413647e1..77368178 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -11,9 +11,10 @@ except ImportError: "http://pypi.python.org/pypi/PIL/) " "for further instructions.") -from skimage.util import img_as_ubyte, img_as_uint, img_as_int, img_as_float +from skimage.util import img_as_ubyte, img_as_uint, img_as_int from six import string_types +from tifffile import imread as tif_imread, imsave as tif_imsave def imread(fname, dtype=None): @@ -28,6 +29,9 @@ def imread(fname, dtype=None): """ + if fname.lower().endswith(('tif', 'tiff')) and dtype is None: + return tif_imread(fname) + im = Image.open(fname) try: # this will raise an IOError if the file is not readable @@ -109,10 +113,6 @@ def ndarray_to_pil(arr, format_str=None): if arr.shape[2] not in (3, 4): raise ValueError("Invalid number of channels in image array.") - if not format_str is None and format_str in ('tiff', 'tif'): - # open with pylibtiff - pass - if arr.ndim == 3: arr = img_as_ubyte(arr) mode_base = mode = {3: 'RGB', 4: 'RGBA'}[arr.shape[2]] @@ -169,8 +169,12 @@ def imsave(fname, arr, format_str=None): if not isinstance(fname, string_types) and format_str is None: format_str = "PNG" - img = ndarray_to_pil(arr, format_str=None) - img.save(fname, format=format_str) + if fname.lower().endswith(('.tiff', '.tif')): + tif_imsave(fname) + + else: + img = ndarray_to_pil(arr, format_str=None) + img.save(fname, format=format_str) def imshow(arr): diff --git a/skimage/io/_plugins/tifffile.py b/skimage/io/_plugins/tifffile.py new file mode 100644 index 00000000..8e094429 --- /dev/null +++ b/skimage/io/_plugins/tifffile.py @@ -0,0 +1,4844 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# tifffile.py + +# Copyright (c) 2008-2014, Christoph Gohlke +# Copyright (c) 2008-2014, The Regents of the University of California +# Produced at the Laboratory for Fluorescence Dynamics +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the copyright holders nor the names of any +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +"""Read and write image data from and to TIFF files. + +Image and metadata can be read from TIFF, BigTIFF, OME-TIFF, STK, LSM, NIH, +SGI, ImageJ, MicroManager, FluoView, SEQ and GEL files. +Only a subset of the TIFF specification is supported, mainly uncompressed +and losslessly compressed 2**(0 to 6) bit integer, 16, 32 and 64-bit float, +grayscale and RGB(A) images, which are commonly used in bio-scientific imaging. +Specifically, reading JPEG and CCITT compressed image data or EXIF, IPTC, GPS, +and XMP metadata is not implemented. +Only primary info records are read for STK, FluoView, MicroManager, and +NIH image formats. + +TIFF, the Tagged Image File Format, is under the control of Adobe Systems. +BigTIFF allows for files greater than 4 GB. STK, LSM, FluoView, SGI, SEQ, GEL, +and OME-TIFF, are custom extensions defined by Molecular Devices (Universal +Imaging Corporation), Carl Zeiss MicroImaging, Olympus, Silicon Graphics +International, Media Cybernetics, Molecular Dynamics, and the Open Microscopy +Environment consortium respectively. + +For command line usage run ``python tifffile.py --help`` + +:Author: + `Christoph Gohlke `_ + +:Organization: + Laboratory for Fluorescence Dynamics, University of California, Irvine + +:Version: 2014.08.24 + +Requirements +------------ +* `CPython 2.7 or 3.4 `_ +* `Numpy 1.8.2 `_ +* `Matplotlib 1.4 `_ (optional for plotting) +* `Tifffile.c 2013.11.05 `_ + (recommended for faster decoding of PackBits and LZW encoded strings) + +Notes +----- +The API is not stable yet and might change between revisions. + +Tested on little-endian platforms only. + +Other Python packages and modules for reading bio-scientific TIFF files: + +* `Imread `_ +* `PyLibTiff `_ +* `SimpleITK `_ +* `PyLSM `_ +* `PyMca.TiffIO.py `_ (same as fabio.TiffIO) +* `BioImageXD.Readers `_ +* `Cellcognition.io `_ +* `CellProfiler.bioformats + `_ + +Acknowledgements +---------------- +* Egor Zindy, University of Manchester, for cz_lsm_scan_info specifics. +* Wim Lewis for a bug fix and some read_cz_lsm functions. +* Hadrien Mary for help on reading MicroManager files. + +References +---------- +(1) TIFF 6.0 Specification and Supplements. Adobe Systems Incorporated. + http://partners.adobe.com/public/developer/tiff/ +(2) TIFF File Format FAQ. http://www.awaresystems.be/imaging/tiff/faq.html +(3) MetaMorph Stack (STK) Image File Format. + http://support.meta.moleculardevices.com/docs/t10243.pdf +(4) Image File Format Description LSM 5/7 Release 6.0 (ZEN 2010). + Carl Zeiss MicroImaging GmbH. BioSciences. May 10, 2011 +(5) File Format Description - LSM 5xx Release 2.0. + http://ibb.gsf.de/homepage/karsten.rodenacker/IDL/Lsmfile.doc +(6) The OME-TIFF format. + http://www.openmicroscopy.org/site/support/file-formats/ome-tiff +(7) UltraQuant(r) Version 6.0 for Windows Start-Up Guide. + http://www.ultralum.com/images%20ultralum/pdf/UQStart%20Up%20Guide.pdf +(8) Micro-Manager File Formats. + http://www.micro-manager.org/wiki/Micro-Manager_File_Formats +(9) Tags for TIFF and Related Specifications. Digital Preservation. + http://www.digitalpreservation.gov/formats/content/tiff_tags.shtml + +Examples +-------- +>>> data = numpy.random.rand(5, 301, 219) +>>> imsave('temp.tif', data) + +>>> image = imread('temp.tif') +>>> numpy.testing.assert_array_equal(image, data) + +>>> with TiffFile('temp.tif') as tif: +... images = tif.asarray() +... for page in tif: +... for tag in page.tags.values(): +... t = tag.name, tag.value +... image = page.asarray() + +""" + +from __future__ import division, print_function + +import sys +import os +import re +import glob +import math +import zlib +import time +import json +import struct +import warnings +import tempfile +import datetime +import collections +from fractions import Fraction +from xml.etree import cElementTree as etree + +import numpy + +try: + import _tifffile +except ImportError: + warnings.warn( + "failed to import the optional _tifffile C extension module.\n" + "Loading of some compressed images will be slow.\n" + "Tifffile.c can be obtained at http://www.lfd.uci.edu/~gohlke/") + +__version__ = '2014.08.24' +__docformat__ = 'restructuredtext en' +__all__ = ('imsave', 'imread', 'imshow', 'TiffFile', 'TiffWriter', + 'TiffSequence') + + +def imsave(filename, data, **kwargs): + """Write image data to TIFF file. + + Refer to the TiffWriter class and member functions for documentation. + + Parameters + ---------- + filename : str + Name of file to write. + data : array_like + Input image. The last dimensions are assumed to be image depth, + height, width, and samples. + kwargs : dict + Parameters 'byteorder', 'bigtiff', and 'software' are passed to + the TiffWriter class. + Parameters 'photometric', 'planarconfig', 'resolution', + 'description', 'compress', 'volume', and 'extratags' are passed to + the TiffWriter.save function. + + Examples + -------- + >>> data = numpy.random.rand(2, 5, 3, 301, 219) + >>> description = u'{"shape": %s}' % str(list(data.shape)) + >>> imsave('temp.tif', data, compress=6, + ... extratags=[(270, 's', 0, description, True)]) + + """ + tifargs = {} + for key in ('byteorder', 'bigtiff', 'software', 'writeshape'): + if key in kwargs: + tifargs[key] = kwargs[key] + del kwargs[key] + + if 'writeshape' not in kwargs: + kwargs['writeshape'] = True + if 'bigtiff' not in tifargs and data.size*data.dtype.itemsize > 2000*2**20: + tifargs['bigtiff'] = True + + with TiffWriter(filename, **tifargs) as tif: + tif.save(data, **kwargs) + + +class TiffWriter(object): + """Write image data to TIFF file. + + TiffWriter instances must be closed using the close method, which is + automatically called when using the 'with' statement. + + Examples + -------- + >>> data = numpy.random.rand(2, 5, 3, 301, 219) + >>> with TiffWriter('temp.tif', bigtiff=True) as tif: + ... for i in range(data.shape[0]): + ... tif.save(data[i], compress=6) + + """ + TYPES = {'B': 1, 's': 2, 'H': 3, 'I': 4, '2I': 5, 'b': 6, + 'h': 8, 'i': 9, 'f': 11, 'd': 12, 'Q': 16, 'q': 17} + TAGS = { + 'new_subfile_type': 254, 'subfile_type': 255, + 'image_width': 256, 'image_length': 257, 'bits_per_sample': 258, + 'compression': 259, 'photometric': 262, 'fill_order': 266, + 'document_name': 269, 'image_description': 270, 'strip_offsets': 273, + 'orientation': 274, 'samples_per_pixel': 277, 'rows_per_strip': 278, + 'strip_byte_counts': 279, 'x_resolution': 282, 'y_resolution': 283, + 'planar_configuration': 284, 'page_name': 285, 'resolution_unit': 296, + 'software': 305, 'datetime': 306, 'predictor': 317, 'color_map': 320, + 'tile_width': 322, 'tile_length': 323, 'tile_offsets': 324, + 'tile_byte_counts': 325, 'extra_samples': 338, 'sample_format': 339, + 'image_depth': 32997, 'tile_depth': 32998} + + def __init__(self, filename, bigtiff=False, byteorder=None, + software='tifffile.py'): + """Create a new TIFF file for writing. + + Use bigtiff=True when creating files greater than 2 GB. + + Parameters + ---------- + filename : str + Name of file to write. + bigtiff : bool + If True, the BigTIFF format is used. + byteorder : {'<', '>'} + The endianness of the data in the file. + By default this is the system's native byte order. + software : str + Name of the software used to create the image. + Saved with the first page only. + + """ + if byteorder not in (None, '<', '>'): + raise ValueError("invalid byteorder %s" % byteorder) + if byteorder is None: + byteorder = '<' if sys.byteorder == 'little' else '>' + + self._byteorder = byteorder + self._software = software + + self._fh = open(filename, 'wb') + self._fh.write({'<': b'II', '>': b'MM'}[byteorder]) + + if bigtiff: + self._bigtiff = True + self._offset_size = 8 + self._tag_size = 20 + self._numtag_format = 'Q' + self._offset_format = 'Q' + self._val_format = '8s' + self._fh.write(struct.pack(byteorder+'HHH', 43, 8, 0)) + else: + self._bigtiff = False + self._offset_size = 4 + self._tag_size = 12 + self._numtag_format = 'H' + self._offset_format = 'I' + self._val_format = '4s' + self._fh.write(struct.pack(byteorder+'H', 42)) + + # first IFD + self._ifd_offset = self._fh.tell() + self._fh.write(struct.pack(byteorder+self._offset_format, 0)) + + def save(self, data, photometric=None, planarconfig=None, resolution=None, + description=None, volume=False, writeshape=False, compress=0, + extratags=()): + """Write image data to TIFF file. + + Image data are written in one stripe per plane. + Dimensions larger than 2 to 4 (depending on photometric mode, planar + configuration, and SGI mode) are flattened and saved as separate pages. + The 'sample_format' and 'bits_per_sample' TIFF tags are derived from + the data type. + + Parameters + ---------- + data : array_like + Input image. The last dimensions are assumed to be image depth, + height, width, and samples. + photometric : {'minisblack', 'miniswhite', 'rgb'} + The color space of the image data. + By default this setting is inferred from the data shape. + planarconfig : {'contig', 'planar'} + Specifies if samples are stored contiguous or in separate planes. + By default this setting is inferred from the data shape. + 'contig': last dimension contains samples. + 'planar': third last dimension contains samples. + resolution : (float, float) or ((int, int), (int, int)) + X and Y resolution in dots per inch as float or rational numbers. + description : str + The subject of the image. Saved with the first page only. + compress : int + Values from 0 to 9 controlling the level of zlib compression. + If 0, data are written uncompressed (default). + volume : bool + If True, volume data are stored in one tile (if applicable) using + the SGI image_depth and tile_depth tags. + Image width and depth must be multiple of 16. + Few software can read this format, e.g. MeVisLab. + writeshape : bool + If True, write the data shape to the image_description tag + if necessary and no other description is given. + extratags: sequence of tuples + Additional tags as [(code, dtype, count, value, writeonce)]. + + code : int + The TIFF tag Id. + dtype : str + Data type of items in 'value' in Python struct format. + One of B, s, H, I, 2I, b, h, i, f, d, Q, or q. + count : int + Number of data values. Not used for string values. + value : sequence + 'Count' values compatible with 'dtype'. + writeonce : bool + If True, the tag is written to the first page only. + + """ + if photometric not in (None, 'minisblack', 'miniswhite', 'rgb'): + raise ValueError("invalid photometric %s" % photometric) + if planarconfig not in (None, 'contig', 'planar'): + raise ValueError("invalid planarconfig %s" % planarconfig) + if not 0 <= compress <= 9: + raise ValueError("invalid compression level %s" % compress) + + fh = self._fh + byteorder = self._byteorder + numtag_format = self._numtag_format + val_format = self._val_format + offset_format = self._offset_format + offset_size = self._offset_size + tag_size = self._tag_size + + data = numpy.asarray(data, dtype=byteorder+data.dtype.char, order='C') + data_shape = shape = data.shape + data = numpy.atleast_2d(data) + + # normalize shape of data + samplesperpixel = 1 + extrasamples = 0 + if volume and data.ndim < 3: + volume = False + if photometric is None: + if planarconfig: + photometric = 'rgb' + elif data.ndim > 2 and shape[-1] in (3, 4): + photometric = 'rgb' + elif volume and data.ndim > 3 and shape[-4] in (3, 4): + photometric = 'rgb' + elif data.ndim > 2 and shape[-3] in (3, 4): + photometric = 'rgb' + else: + photometric = 'minisblack' + if planarconfig and len(shape) <= (3 if volume else 2): + planarconfig = None + photometric = 'minisblack' + if photometric == 'rgb': + if len(shape) < 3: + raise ValueError("not a RGB(A) image") + if len(shape) < 4: + volume = False + if planarconfig is None: + if shape[-1] in (3, 4): + planarconfig = 'contig' + elif shape[-4 if volume else -3] in (3, 4): + planarconfig = 'planar' + elif shape[-1] > shape[-4 if volume else -3]: + planarconfig = 'planar' + else: + planarconfig = 'contig' + if planarconfig == 'contig': + data = data.reshape((-1, 1) + shape[(-4 if volume else -3):]) + samplesperpixel = data.shape[-1] + else: + data = data.reshape( + (-1,) + shape[(-4 if volume else -3):] + (1,)) + samplesperpixel = data.shape[1] + if samplesperpixel > 3: + extrasamples = samplesperpixel - 3 + elif planarconfig and len(shape) > (3 if volume else 2): + if planarconfig == 'contig': + data = data.reshape((-1, 1) + shape[(-4 if volume else -3):]) + samplesperpixel = data.shape[-1] + else: + data = data.reshape( + (-1,) + shape[(-4 if volume else -3):] + (1,)) + samplesperpixel = data.shape[1] + extrasamples = samplesperpixel - 1 + else: + planarconfig = None + # remove trailing 1s + while len(shape) > 2 and shape[-1] == 1: + shape = shape[:-1] + if len(shape) < 3: + volume = False + if False and ( + len(shape) > (3 if volume else 2) and shape[-1] < 5 and + all(shape[-1] < i + for i in shape[(-4 if volume else -3):-1])): + # DISABLED: non-standard TIFF, e.g. (220, 320, 2) + planarconfig = 'contig' + samplesperpixel = shape[-1] + data = data.reshape((-1, 1) + shape[(-4 if volume else -3):]) + else: + data = data.reshape( + (-1, 1) + shape[(-3 if volume else -2):] + (1,)) + + if samplesperpixel == 2: + warnings.warn("writing non-standard TIFF (samplesperpixel 2)") + + if volume and (data.shape[-2] % 16 or data.shape[-3] % 16): + warnings.warn("volume width or length are not multiple of 16") + volume = False + data = numpy.swapaxes(data, 1, 2) + data = data.reshape( + (data.shape[0] * data.shape[1],) + data.shape[2:]) + + # data.shape is now normalized 5D or 6D, depending on volume + # (pages, planar_samples, (depth,) height, width, contig_samples) + assert len(data.shape) in (5, 6) + shape = data.shape + + bytestr = bytes if sys.version[0] == '2' else ( + lambda x: bytes(x, 'utf-8') if isinstance(x, str) else x) + tags = [] # list of (code, ifdentry, ifdvalue, writeonce) + + if volume: + # use tiles to save volume data + tag_byte_counts = TiffWriter.TAGS['tile_byte_counts'] + tag_offsets = TiffWriter.TAGS['tile_offsets'] + else: + # else use strips + tag_byte_counts = TiffWriter.TAGS['strip_byte_counts'] + tag_offsets = TiffWriter.TAGS['strip_offsets'] + + def pack(fmt, *val): + return struct.pack(byteorder+fmt, *val) + + def addtag(code, dtype, count, value, writeonce=False): + # Compute ifdentry & ifdvalue bytes from code, dtype, count, value. + # Append (code, ifdentry, ifdvalue, writeonce) to tags list. + code = int(TiffWriter.TAGS.get(code, code)) + try: + tifftype = TiffWriter.TYPES[dtype] + except KeyError: + raise ValueError("unknown dtype %s" % dtype) + rawcount = count + if dtype == 's': + value = bytestr(value) + b'\0' + count = rawcount = len(value) + value = (value, ) + if len(dtype) > 1: + count *= int(dtype[:-1]) + dtype = dtype[-1] + ifdentry = [pack('HH', code, tifftype), + pack(offset_format, rawcount)] + ifdvalue = None + if count == 1: + if isinstance(value, (tuple, list)): + value = value[0] + ifdentry.append(pack(val_format, pack(dtype, value))) + elif struct.calcsize(dtype) * count <= offset_size: + ifdentry.append(pack(val_format, + pack(str(count)+dtype, *value))) + else: + ifdentry.append(pack(offset_format, 0)) + ifdvalue = pack(str(count)+dtype, *value) + tags.append((code, b''.join(ifdentry), ifdvalue, writeonce)) + + def rational(arg, max_denominator=1000000): + # return nominator and denominator from float or two integers + try: + f = Fraction.from_float(arg) + except TypeError: + f = Fraction(arg[0], arg[1]) + f = f.limit_denominator(max_denominator) + return f.numerator, f.denominator + + if self._software: + addtag('software', 's', 0, self._software, writeonce=True) + self._software = None # only save to first page + if description: + addtag('image_description', 's', 0, description, writeonce=True) + elif writeshape and shape[0] > 1 and shape != data_shape: + addtag('image_description', 's', 0, + "shape=(%s)" % (",".join('%i' % i for i in data_shape)), + writeonce=True) + addtag('datetime', 's', 0, + datetime.datetime.now().strftime("%Y:%m:%d %H:%M:%S"), + writeonce=True) + addtag('compression', 'H', 1, 32946 if compress else 1) + addtag('orientation', 'H', 1, 1) + addtag('image_width', 'I', 1, shape[-2]) + addtag('image_length', 'I', 1, shape[-3]) + if volume: + addtag('image_depth', 'I', 1, shape[-4]) + addtag('tile_depth', 'I', 1, shape[-4]) + addtag('tile_width', 'I', 1, shape[-2]) + addtag('tile_length', 'I', 1, shape[-3]) + addtag('new_subfile_type', 'I', 1, 0 if shape[0] == 1 else 2) + addtag('sample_format', 'H', 1, + {'u': 1, 'i': 2, 'f': 3, 'c': 6}[data.dtype.kind]) + addtag('photometric', 'H', 1, + {'miniswhite': 0, 'minisblack': 1, 'rgb': 2}[photometric]) + addtag('samples_per_pixel', 'H', 1, samplesperpixel) + if planarconfig and samplesperpixel > 1: + addtag('planar_configuration', 'H', 1, 1 + if planarconfig == 'contig' else 2) + addtag('bits_per_sample', 'H', samplesperpixel, + (data.dtype.itemsize * 8, ) * samplesperpixel) + else: + addtag('bits_per_sample', 'H', 1, data.dtype.itemsize * 8) + if extrasamples: + if photometric == 'rgb' and extrasamples == 1: + addtag('extra_samples', 'H', 1, 1) # associated alpha channel + else: + addtag('extra_samples', 'H', extrasamples, (0,) * extrasamples) + if resolution: + addtag('x_resolution', '2I', 1, rational(resolution[0])) + addtag('y_resolution', '2I', 1, rational(resolution[1])) + addtag('resolution_unit', 'H', 1, 2) + addtag('rows_per_strip', 'I', 1, + shape[-3] * (shape[-4] if volume else 1)) + + # use one strip or tile per plane + strip_byte_counts = (data[0, 0].size * data.dtype.itemsize,) * shape[1] + addtag(tag_byte_counts, offset_format, shape[1], strip_byte_counts) + addtag(tag_offsets, offset_format, shape[1], (0, ) * shape[1]) + + # add extra tags from users + for t in extratags: + addtag(*t) + # the entries in an IFD must be sorted in ascending order by tag code + tags = sorted(tags, key=lambda x: x[0]) + + if not self._bigtiff and (fh.tell() + data.size*data.dtype.itemsize + > 2**31-1): + raise ValueError("data too large for non-bigtiff file") + + for pageindex in range(shape[0]): + # update pointer at ifd_offset + pos = fh.tell() + fh.seek(self._ifd_offset) + fh.write(pack(offset_format, pos)) + fh.seek(pos) + + # write ifdentries + fh.write(pack(numtag_format, len(tags))) + tag_offset = fh.tell() + fh.write(b''.join(t[1] for t in tags)) + self._ifd_offset = fh.tell() + fh.write(pack(offset_format, 0)) # offset to next IFD + + # write tag values and patch offsets in ifdentries, if necessary + for tagindex, tag in enumerate(tags): + if tag[2]: + pos = fh.tell() + fh.seek(tag_offset + tagindex*tag_size + offset_size + 4) + fh.write(pack(offset_format, pos)) + fh.seek(pos) + if tag[0] == tag_offsets: + strip_offsets_offset = pos + elif tag[0] == tag_byte_counts: + strip_byte_counts_offset = pos + fh.write(tag[2]) + + # write image data + data_offset = fh.tell() + if compress: + strip_byte_counts = [] + for plane in data[pageindex]: + plane = zlib.compress(plane, compress) + strip_byte_counts.append(len(plane)) + fh.write(plane) + else: + # if this fails try update Python/numpy + data[pageindex].tofile(fh) + fh.flush() + + # update strip and tile offsets and byte_counts if necessary + pos = fh.tell() + for tagindex, tag in enumerate(tags): + if tag[0] == tag_offsets: # strip or tile offsets + if tag[2]: + fh.seek(strip_offsets_offset) + strip_offset = data_offset + for size in strip_byte_counts: + fh.write(pack(offset_format, strip_offset)) + strip_offset += size + else: + fh.seek(tag_offset + tagindex*tag_size + + offset_size + 4) + fh.write(pack(offset_format, data_offset)) + elif tag[0] == tag_byte_counts: # strip or tile byte_counts + if compress: + if tag[2]: + fh.seek(strip_byte_counts_offset) + for size in strip_byte_counts: + fh.write(pack(offset_format, size)) + else: + fh.seek(tag_offset + tagindex*tag_size + + offset_size + 4) + fh.write(pack(offset_format, strip_byte_counts[0])) + break + fh.seek(pos) + fh.flush() + # remove tags that should be written only once + if pageindex == 0: + tags = [t for t in tags if not t[-1]] + + def close(self): + self._fh.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + +def imread(files, **kwargs): + """Return image data from TIFF file(s) as numpy array. + + The first image series is returned if no arguments are provided. + + Parameters + ---------- + files : str or list + File name, glob pattern, or list of file names. + key : int, slice, or sequence of page indices + Defines which pages to return as array. + series : int + Defines which series of pages in file to return as array. + multifile : bool + If True (default), OME-TIFF data may include pages from multiple files. + pattern : str + Regular expression pattern that matches axes names and indices in + file names. + kwargs : dict + Additional parameters passed to the TiffFile or TiffSequence asarray + function. + + Examples + -------- + >>> im = imread('test.tif', key=0) + >>> im.shape + (256, 256, 4) + >>> ims = imread(['test.tif', 'test.tif']) + >>> ims.shape + (2, 256, 256, 4) + + """ + kwargs_file = {} + if 'multifile' in kwargs: + kwargs_file['multifile'] = kwargs['multifile'] + del kwargs['multifile'] + else: + kwargs_file['multifile'] = True + kwargs_seq = {} + if 'pattern' in kwargs: + kwargs_seq['pattern'] = kwargs['pattern'] + del kwargs['pattern'] + + if isinstance(files, basestring) and any(i in files for i in '?*'): + files = glob.glob(files) + if not files: + raise ValueError('no files found') + if len(files) == 1: + files = files[0] + + if isinstance(files, basestring): + with TiffFile(files, **kwargs_file) as tif: + return tif.asarray(**kwargs) + else: + with TiffSequence(files, **kwargs_seq) as imseq: + return imseq.asarray(**kwargs) + + +class lazyattr(object): + """Lazy object attribute whose value is computed on first access.""" + __slots__ = ('func', ) + + def __init__(self, func): + self.func = func + + def __get__(self, instance, owner): + if instance is None: + return self + value = self.func(instance) + if value is NotImplemented: + return getattr(super(owner, instance), self.func.__name__) + setattr(instance, self.func.__name__, value) + return value + + +class TiffFile(object): + """Read image and metadata from TIFF, STK, LSM, and FluoView files. + + TiffFile instances must be closed using the close method, which is + automatically called when using the 'with' statement. + + Attributes + ---------- + pages : list + All TIFF pages in file. + series : list of Records(shape, dtype, axes, TiffPages) + TIFF pages with compatible shapes and types. + micromanager_metadata: dict + Extra MicroManager non-TIFF metadata in the file, if exists. + + All attributes are read-only. + + Examples + -------- + >>> with TiffFile('test.tif') as tif: + ... data = tif.asarray() + ... data.shape + (256, 256, 4) + + """ + def __init__(self, arg, name=None, offset=None, size=None, + multifile=True, multifile_close=True): + """Initialize instance from file. + + Parameters + ---------- + arg : str or open file + Name of file or open file object. + The file objects are closed in TiffFile.close(). + name : str + Optional name of file in case 'arg' is a file handle. + offset : int + Optional start position of embedded file. By default this is + the current file position. + size : int + Optional size of embedded file. By default this is the number + of bytes from the 'offset' to the end of the file. + multifile : bool + If True (default), series may include pages from multiple files. + Currently applies to OME-TIFF only. + multifile_close : bool + If True (default), keep the handles of other files in multifile + series closed. This is inefficient when few files refer to + many pages. If False, the C runtime may run out of resources. + + """ + self._fh = FileHandle(arg, name=name, offset=offset, size=size) + self.offset_size = None + self.pages = [] + self._multifile = bool(multifile) + self._multifile_close = bool(multifile_close) + self._files = {self._fh.name: self} # cache of TiffFiles + try: + self._fromfile() + except Exception: + self._fh.close() + raise + + @property + def filehandle(self): + """Return file handle.""" + return self._fh + + @property + def filename(self): + """Return name of file handle.""" + return self._fh.name + + def close(self): + """Close open file handle(s).""" + for tif in self._files.values(): + tif._fh.close() + self._files = {} + + def _fromfile(self): + """Read TIFF header and all page records from file.""" + self._fh.seek(0) + try: + self.byteorder = {b'II': '<', b'MM': '>'}[self._fh.read(2)] + except KeyError: + raise ValueError("not a valid TIFF file") + version = struct.unpack(self.byteorder+'H', self._fh.read(2))[0] + if version == 43: # BigTiff + self.offset_size, zero = struct.unpack(self.byteorder+'HH', + self._fh.read(4)) + if zero or self.offset_size != 8: + raise ValueError("not a valid BigTIFF file") + elif version == 42: + self.offset_size = 4 + else: + raise ValueError("not a TIFF file") + self.pages = [] + while True: + try: + page = TiffPage(self) + self.pages.append(page) + except StopIteration: + break + if not self.pages: + raise ValueError("empty TIFF file") + + if self.is_micromanager: + # MicroManager files contain metadata not stored in TIFF tags. + self.micromanager_metadata = read_micromanager_metadata(self._fh) + + if self.is_lsm: + self._fix_lsm_strip_offsets() + self._fix_lsm_strip_byte_counts() + + def _fix_lsm_strip_offsets(self): + """Unwrap strip offsets for LSM files greater than 4 GB.""" + for series in self.series: + wrap = 0 + previous_offset = 0 + for page in series.pages: + strip_offsets = [] + for current_offset in page.strip_offsets: + if current_offset < previous_offset: + wrap += 2**32 + strip_offsets.append(current_offset + wrap) + previous_offset = current_offset + page.strip_offsets = tuple(strip_offsets) + + def _fix_lsm_strip_byte_counts(self): + """Set strip_byte_counts to size of compressed data. + + The strip_byte_counts tag in LSM files contains the number of bytes + for the uncompressed data. + + """ + if not self.pages: + return + strips = {} + for page in self.pages: + assert len(page.strip_offsets) == len(page.strip_byte_counts) + for offset, bytecount in zip(page.strip_offsets, + page.strip_byte_counts): + strips[offset] = bytecount + offsets = sorted(strips.keys()) + offsets.append(min(offsets[-1] + strips[offsets[-1]], self._fh.size)) + for i, offset in enumerate(offsets[:-1]): + strips[offset] = min(strips[offset], offsets[i+1] - offset) + for page in self.pages: + if page.compression: + page.strip_byte_counts = tuple( + strips[offset] for offset in page.strip_offsets) + + @lazyattr + def series(self): + """Return series of TiffPage with compatible shape and properties.""" + if not self.pages: + return [] + + series = [] + page0 = self.pages[0] + + if self.is_ome: + series = self._omeseries() + elif self.is_fluoview: + dims = {b'X': 'X', b'Y': 'Y', b'Z': 'Z', b'T': 'T', + b'WAVELENGTH': 'C', b'TIME': 'T', b'XY': 'R', + b'EVENT': 'V', b'EXPOSURE': 'L'} + mmhd = list(reversed(page0.mm_header.dimensions)) + series = [Record( + axes=''.join(dims.get(i[0].strip().upper(), 'Q') + for i in mmhd if i[1] > 1), + shape=tuple(int(i[1]) for i in mmhd if i[1] > 1), + pages=self.pages, dtype=numpy.dtype(page0.dtype))] + elif self.is_lsm: + lsmi = page0.cz_lsm_info + axes = CZ_SCAN_TYPES[lsmi.scan_type] + if page0.is_rgb: + axes = axes.replace('C', '').replace('XY', 'XYC') + axes = axes[::-1] + shape = tuple(getattr(lsmi, CZ_DIMENSIONS[i]) for i in axes) + pages = [p for p in self.pages if not p.is_reduced] + series = [Record(axes=axes, shape=shape, pages=pages, + dtype=numpy.dtype(pages[0].dtype))] + if len(pages) != len(self.pages): # reduced RGB pages + pages = [p for p in self.pages if p.is_reduced] + cp = 1 + i = 0 + while cp < len(pages) and i < len(shape)-2: + cp *= shape[i] + i += 1 + shape = shape[:i] + pages[0].shape + axes = axes[:i] + 'CYX' + series.append(Record(axes=axes, shape=shape, pages=pages, + dtype=numpy.dtype(pages[0].dtype))) + elif self.is_imagej: + shape = [] + axes = [] + ij = page0.imagej_tags + if 'frames' in ij: + shape.append(ij['frames']) + axes.append('T') + if 'slices' in ij: + shape.append(ij['slices']) + axes.append('Z') + if 'channels' in ij and not self.is_rgb: + shape.append(ij['channels']) + axes.append('C') + remain = len(self.pages) // (product(shape) if shape else 1) + if remain > 1: + shape.append(remain) + axes.append('I') + shape.extend(page0.shape) + axes.extend(page0.axes) + axes = ''.join(axes) + series = [Record(pages=self.pages, shape=tuple(shape), axes=axes, + dtype=numpy.dtype(page0.dtype))] + elif self.is_nih: + if len(self.pages) == 1: + shape = page0.shape + axes = page0.axes + else: + shape = (len(self.pages),) + page0.shape + axes = 'I' + page0.axes + series = [Record(pages=self.pages, shape=shape, axes=axes, + dtype=numpy.dtype(page0.dtype))] + elif page0.is_shaped: + # TODO: shaped files can contain multiple series + shape = page0.tags['image_description'].value[7:-1] + shape = tuple(int(i) for i in shape.split(b',')) + series = [Record(pages=self.pages, shape=shape, + axes='Q' * len(shape), + dtype=numpy.dtype(page0.dtype))] + + # generic detection of series + if not series: + shapes = [] + pages = {} + for page in self.pages: + if not page.shape: + continue + shape = page.shape + (page.axes, + page.compression in TIFF_DECOMPESSORS) + if shape not in pages: + shapes.append(shape) + pages[shape] = [page] + else: + pages[shape].append(page) + series = [Record(pages=pages[s], + axes=(('I' + s[-2]) + if len(pages[s]) > 1 else s[-2]), + dtype=numpy.dtype(pages[s][0].dtype), + shape=((len(pages[s]), ) + s[:-2] + if len(pages[s]) > 1 else s[:-2])) + for s in shapes] + + # remove empty series, e.g. in MD Gel files + series = [s for s in series if sum(s.shape) > 0] + + return series + + def asarray(self, key=None, series=None, memmap=False): + """Return image data from multiple TIFF pages as numpy array. + + By default the first image series is returned. + + Parameters + ---------- + key : int, slice, or sequence of page indices + Defines which pages to return as array. + series : int + Defines which series of pages to return as array. + memmap : bool + If True, return an array stored in a binary file on disk + if possible. + + """ + if key is None and series is None: + series = 0 + if series is not None: + pages = self.series[series].pages + else: + pages = self.pages + + if key is None: + pass + elif isinstance(key, int): + pages = [pages[key]] + elif isinstance(key, slice): + pages = pages[key] + elif isinstance(key, collections.Iterable): + pages = [pages[k] for k in key] + else: + raise TypeError("key must be an int, slice, or sequence") + + if not len(pages): + raise ValueError("no pages selected") + + if self.is_nih: + if pages[0].is_palette: + result = stack_pages(pages, colormapped=False, squeeze=False) + result = numpy.take(pages[0].color_map, result, axis=1) + result = numpy.swapaxes(result, 0, 1) + else: + result = stack_pages(pages, memmap=memmap, + colormapped=False, squeeze=False) + elif len(pages) == 1: + return pages[0].asarray(memmap=memmap) + elif self.is_ome: + assert not self.is_palette, "color mapping disabled for ome-tiff" + if any(p is None for p in pages): + # zero out missing pages + firstpage = next(p for p in pages if p) + nopage = numpy.zeros_like( + firstpage.asarray(memmap=False)) + s = self.series[series] + if memmap: + with tempfile.NamedTemporaryFile() as fh: + result = numpy.memmap(fh, dtype=s.dtype, shape=s.shape) + result = result.reshape(-1) + else: + result = numpy.empty(s.shape, s.dtype).reshape(-1) + index = 0 + + class KeepOpen: + # keep Tiff files open between consecutive pages + def __init__(self, parent, close): + self.master = parent + self.parent = parent + self._close = close + + def open(self, page): + if self._close and page and page.parent != self.parent: + if self.parent != self.master: + self.parent.filehandle.close() + self.parent = page.parent + self.parent.filehandle.open() + + def close(self): + if self._close and self.parent != self.master: + self.parent.filehandle.close() + + keep = KeepOpen(self, self._multifile_close) + for page in pages: + keep.open(page) + if page: + a = page.asarray(memmap=False, colormapped=False, + reopen=False) + else: + a = nopage + try: + result[index:index + a.size] = a.reshape(-1) + except ValueError as e: + warnings.warn("ome-tiff: %s" % e) + break + index += a.size + keep.close() + else: + result = stack_pages(pages, memmap=memmap) + + if key is None: + try: + result.shape = self.series[series].shape + except ValueError: + try: + warnings.warn("failed to reshape %s to %s" % ( + result.shape, self.series[series].shape)) + # try series of expected shapes + result.shape = (-1,) + self.series[series].shape + except ValueError: + # revert to generic shape + result.shape = (-1,) + pages[0].shape + else: + result.shape = (-1,) + pages[0].shape + return result + + def _omeseries(self): + """Return image series in OME-TIFF file(s).""" + root = etree.fromstring(self.pages[0].tags['image_description'].value) + uuid = root.attrib.get('UUID', None) + self._files = {uuid: self} + dirname = self._fh.dirname + modulo = {} + result = [] + for element in root: + if element.tag.endswith('BinaryOnly'): + warnings.warn("ome-xml: not an ome-tiff master file") + break + if element.tag.endswith('StructuredAnnotations'): + for annot in element: + if not annot.attrib.get('Namespace', + '').endswith('modulo'): + continue + for value in annot: + for modul in value: + for along in modul: + if not along.tag[:-1].endswith('Along'): + continue + axis = along.tag[-1] + newaxis = along.attrib.get('Type', 'other') + newaxis = AXES_LABELS[newaxis] + if 'Start' in along.attrib: + labels = range( + int(along.attrib['Start']), + int(along.attrib['End']) + 1, + int(along.attrib.get('Step', 1))) + else: + labels = [label.text for label in along + if label.tag.endswith('Label')] + modulo[axis] = (newaxis, labels) + if not element.tag.endswith('Image'): + continue + for pixels in element: + if not pixels.tag.endswith('Pixels'): + continue + atr = pixels.attrib + dtype = atr.get('Type', None) + axes = ''.join(reversed(atr['DimensionOrder'])) + shape = list(int(atr['Size'+ax]) for ax in axes) + size = product(shape[:-2]) + ifds = [None] * size + for data in pixels: + if not data.tag.endswith('TiffData'): + continue + atr = data.attrib + ifd = int(atr.get('IFD', 0)) + num = int(atr.get('NumPlanes', 1 if 'IFD' in atr else 0)) + num = int(atr.get('PlaneCount', num)) + idx = [int(atr.get('First'+ax, 0)) for ax in axes[:-2]] + try: + idx = numpy.ravel_multi_index(idx, shape[:-2]) + except ValueError: + # ImageJ produces invalid ome-xml when cropping + warnings.warn("ome-xml: invalid TiffData index") + continue + for uuid in data: + if not uuid.tag.endswith('UUID'): + continue + if uuid.text not in self._files: + if not self._multifile: + # abort reading multifile OME series + # and fall back to generic series + return [] + fname = uuid.attrib['FileName'] + try: + tif = TiffFile(os.path.join(dirname, fname)) + except (IOError, ValueError): + tif.close() + warnings.warn( + "ome-xml: failed to read '%s'" % fname) + break + self._files[uuid.text] = tif + if self._multifile_close: + tif.close() + pages = self._files[uuid.text].pages + try: + for i in range(num if num else len(pages)): + ifds[idx + i] = pages[ifd + i] + except IndexError: + warnings.warn("ome-xml: index out of range") + # only process first uuid + break + else: + pages = self.pages + try: + for i in range(num if num else len(pages)): + ifds[idx + i] = pages[ifd + i] + except IndexError: + warnings.warn("ome-xml: index out of range") + if all(i is None for i in ifds): + # skip images without data + continue + dtype = next(i for i in ifds if i).dtype + result.append(Record(axes=axes, shape=shape, pages=ifds, + dtype=numpy.dtype(dtype))) + + for record in result: + for axis, (newaxis, labels) in modulo.items(): + i = record.axes.index(axis) + size = len(labels) + if record.shape[i] == size: + record.axes = record.axes.replace(axis, newaxis, 1) + else: + record.shape[i] //= size + record.shape.insert(i+1, size) + record.axes = record.axes.replace(axis, axis+newaxis, 1) + record.shape = tuple(record.shape) + + # squeeze dimensions + for record in result: + record.shape, record.axes = squeeze_axes(record.shape, record.axes) + + return result + + def __len__(self): + """Return number of image pages in file.""" + return len(self.pages) + + def __getitem__(self, key): + """Return specified page.""" + return self.pages[key] + + def __iter__(self): + """Return iterator over pages.""" + return iter(self.pages) + + def __str__(self): + """Return string containing information about file.""" + result = [ + self._fh.name.capitalize(), + format_size(self._fh.size), + {'<': 'little endian', '>': 'big endian'}[self.byteorder]] + if self.is_bigtiff: + result.append("bigtiff") + if len(self.pages) > 1: + result.append("%i pages" % len(self.pages)) + if len(self.series) > 1: + result.append("%i series" % len(self.series)) + if len(self._files) > 1: + result.append("%i files" % (len(self._files))) + return ", ".join(result) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + @lazyattr + def fstat(self): + try: + return os.fstat(self._fh.fileno()) + except Exception: # io.UnsupportedOperation + return None + + @lazyattr + def is_bigtiff(self): + return self.offset_size != 4 + + @lazyattr + def is_rgb(self): + return all(p.is_rgb for p in self.pages) + + @lazyattr + def is_palette(self): + return all(p.is_palette for p in self.pages) + + @lazyattr + def is_mdgel(self): + return any(p.is_mdgel for p in self.pages) + + @lazyattr + def is_mediacy(self): + return any(p.is_mediacy for p in self.pages) + + @lazyattr + def is_stk(self): + return all(p.is_stk for p in self.pages) + + @lazyattr + def is_lsm(self): + return self.pages[0].is_lsm + + @lazyattr + def is_imagej(self): + return self.pages[0].is_imagej + + @lazyattr + def is_micromanager(self): + return self.pages[0].is_micromanager + + @lazyattr + def is_nih(self): + return self.pages[0].is_nih + + @lazyattr + def is_fluoview(self): + return self.pages[0].is_fluoview + + @lazyattr + def is_ome(self): + return self.pages[0].is_ome + + +class TiffPage(object): + """A TIFF image file directory (IFD). + + Attributes + ---------- + index : int + Index of page in file. + dtype : str {TIFF_SAMPLE_DTYPES} + Data type of image, colormapped if applicable. + shape : tuple + Dimensions of the image array in TIFF page, + colormapped and with one alpha channel if applicable. + axes : str + Axes label codes: + 'X' width, 'Y' height, 'S' sample, 'I' image series|page|plane, + 'Z' depth, 'C' color|em-wavelength|channel, 'E' ex-wavelength|lambda, + 'T' time, 'R' region|tile, 'A' angle, 'P' phase, 'H' lifetime, + 'L' exposure, 'V' event, 'Q' unknown, '_' missing + tags : TiffTags + Dictionary of tags in page. + Tag values are also directly accessible as attributes. + color_map : numpy array + Color look up table, if exists. + cz_lsm_scan_info: Record(dict) + LSM scan info attributes, if exists. + imagej_tags: Record(dict) + Consolidated ImageJ description and metadata tags, if exists. + uic_tags: Record(dict) + Consolidated MetaMorph STK/UIC tags, if exists. + + All attributes are read-only. + + Notes + ----- + The internal, normalized '_shape' attribute is 6 dimensional: + + 0. number planes (stk) + 1. planar samples_per_pixel + 2. image_depth Z (sgi) + 3. image_length Y + 4. image_width X + 5. contig samples_per_pixel + + """ + def __init__(self, parent): + """Initialize instance from file.""" + self.parent = parent + self.index = len(parent.pages) + self.shape = self._shape = () + self.dtype = self._dtype = None + self.axes = "" + self.tags = TiffTags() + + self._fromfile() + self._process_tags() + + def _fromfile(self): + """Read TIFF IFD structure and its tags from file. + + File cursor must be at storage position of IFD offset and is left at + offset to next IFD. + + Raises StopIteration if offset (first bytes read) is 0. + + """ + fh = self.parent.filehandle + byteorder = self.parent.byteorder + offset_size = self.parent.offset_size + + fmt = {4: 'I', 8: 'Q'}[offset_size] + offset = struct.unpack(byteorder + fmt, fh.read(offset_size))[0] + if not offset: + raise StopIteration() + + # read standard tags + tags = self.tags + fh.seek(offset) + fmt, size = {4: ('H', 2), 8: ('Q', 8)}[offset_size] + try: + numtags = struct.unpack(byteorder + fmt, fh.read(size))[0] + except Exception: + warnings.warn("corrupted page list") + raise StopIteration() + + tagcode = 0 + for _ in range(numtags): + try: + tag = TiffTag(self.parent) + # print(tag) + except TiffTag.Error as e: + warnings.warn(str(e)) + continue + if tagcode > tag.code: + # expected for early LSM and tifffile versions + warnings.warn("tags are not ordered by code") + tagcode = tag.code + if tag.name not in tags: + tags[tag.name] = tag + else: + # some files contain multiple IFD with same code + # e.g. MicroManager files contain two image_description + i = 1 + while True: + name = "%s_%i" % (tag.name, i) + if name not in tags: + tags[name] = tag + break + + pos = fh.tell() + + if self.is_lsm or (self.index and self.parent.is_lsm): + # correct non standard LSM bitspersample tags + self.tags['bits_per_sample']._correct_lsm_bitspersample(self) + + if self.is_lsm: + # read LSM info subrecords + for name, reader in CZ_LSM_INFO_READERS.items(): + try: + offset = self.cz_lsm_info['offset_'+name] + except KeyError: + continue + if offset < 8: + # older LSM revision + continue + fh.seek(offset) + try: + setattr(self, 'cz_lsm_'+name, reader(fh)) + except ValueError: + pass + + elif self.is_stk and 'uic1tag' in tags and not tags['uic1tag'].value: + # read uic1tag now that plane count is known + uic1tag = tags['uic1tag'] + fh.seek(uic1tag.value_offset) + tags['uic1tag'].value = Record( + read_uic1tag(fh, byteorder, uic1tag.dtype, uic1tag.count, + tags['uic2tag'].count)) + fh.seek(pos) + + def _process_tags(self): + """Validate standard tags and initialize attributes. + + Raise ValueError if tag values are not supported. + + """ + tags = self.tags + for code, (name, default, dtype, count, validate) in TIFF_TAGS.items(): + if not (name in tags or default is None): + tags[name] = TiffTag(code, dtype=dtype, count=count, + value=default, name=name) + if name in tags and validate: + try: + if tags[name].count == 1: + setattr(self, name, validate[tags[name].value]) + else: + setattr(self, name, tuple( + validate[value] for value in tags[name].value)) + except KeyError: + raise ValueError("%s.value (%s) not supported" % + (name, tags[name].value)) + + tag = tags['bits_per_sample'] + if tag.count == 1: + self.bits_per_sample = tag.value + else: + # LSM might list more items than samples_per_pixel + value = tag.value[:self.samples_per_pixel] + if any((v-value[0] for v in value)): + self.bits_per_sample = value + else: + self.bits_per_sample = value[0] + + tag = tags['sample_format'] + if tag.count == 1: + self.sample_format = TIFF_SAMPLE_FORMATS[tag.value] + else: + value = tag.value[:self.samples_per_pixel] + if any((v-value[0] for v in value)): + self.sample_format = [TIFF_SAMPLE_FORMATS[v] for v in value] + else: + self.sample_format = TIFF_SAMPLE_FORMATS[value[0]] + + if 'photometric' not in tags: + self.photometric = None + + if 'image_depth' not in tags: + self.image_depth = 1 + + if 'image_length' in tags: + self.strips_per_image = int(math.floor( + float(self.image_length + self.rows_per_strip - 1) / + self.rows_per_strip)) + else: + self.strips_per_image = 0 + + key = (self.sample_format, self.bits_per_sample) + self.dtype = self._dtype = TIFF_SAMPLE_DTYPES.get(key, None) + + if 'image_length' not in self.tags or 'image_width' not in self.tags: + # some GEL file pages are missing image data + self.image_length = 0 + self.image_width = 0 + self.image_depth = 0 + self.strip_offsets = 0 + self._shape = () + self.shape = () + self.axes = '' + + if self.is_palette: + self.dtype = self.tags['color_map'].dtype[1] + self.color_map = numpy.array(self.color_map, self.dtype) + dmax = self.color_map.max() + if dmax < 256: + self.dtype = numpy.uint8 + self.color_map = self.color_map.astype(self.dtype) + #else: + # self.dtype = numpy.uint8 + # self.color_map >>= 8 + # self.color_map = self.color_map.astype(self.dtype) + self.color_map.shape = (3, -1) + + # determine shape of data + image_length = self.image_length + image_width = self.image_width + image_depth = self.image_depth + samples_per_pixel = self.samples_per_pixel + + if self.is_stk: + assert self.image_depth == 1 + planes = self.tags['uic2tag'].count + if self.is_contig: + self._shape = (planes, 1, 1, image_length, image_width, + samples_per_pixel) + if samples_per_pixel == 1: + self.shape = (planes, image_length, image_width) + self.axes = 'YX' + else: + self.shape = (planes, image_length, image_width, + samples_per_pixel) + self.axes = 'YXS' + else: + self._shape = (planes, samples_per_pixel, 1, image_length, + image_width, 1) + if samples_per_pixel == 1: + self.shape = (planes, image_length, image_width) + self.axes = 'YX' + else: + self.shape = (planes, samples_per_pixel, image_length, + image_width) + self.axes = 'SYX' + # detect type of series + if planes == 1: + self.shape = self.shape[1:] + elif numpy.all(self.uic2tag.z_distance != 0): + self.axes = 'Z' + self.axes + elif numpy.all(numpy.diff(self.uic2tag.time_created) != 0): + self.axes = 'T' + self.axes + else: + self.axes = 'I' + self.axes + # DISABLED + if self.is_palette: + assert False, "color mapping disabled for stk" + if self.color_map.shape[1] >= 2**self.bits_per_sample: + if image_depth == 1: + self.shape = (3, planes, image_length, image_width) + else: + self.shape = (3, planes, image_depth, image_length, + image_width) + self.axes = 'C' + self.axes + else: + warnings.warn("palette cannot be applied") + self.is_palette = False + elif self.is_palette: + samples = 1 + if 'extra_samples' in self.tags: + samples += len(self.extra_samples) + if self.is_contig: + self._shape = (1, 1, image_depth, image_length, image_width, + samples) + else: + self._shape = (1, samples, image_depth, image_length, + image_width, 1) + if self.color_map.shape[1] >= 2**self.bits_per_sample: + if image_depth == 1: + self.shape = (3, image_length, image_width) + self.axes = 'CYX' + else: + self.shape = (3, image_depth, image_length, image_width) + self.axes = 'CZYX' + else: + warnings.warn("palette cannot be applied") + self.is_palette = False + if image_depth == 1: + self.shape = (image_length, image_width) + self.axes = 'YX' + else: + self.shape = (image_depth, image_length, image_width) + self.axes = 'ZYX' + elif self.is_rgb or samples_per_pixel > 1: + if self.is_contig: + self._shape = (1, 1, image_depth, image_length, image_width, + samples_per_pixel) + if image_depth == 1: + self.shape = (image_length, image_width, samples_per_pixel) + self.axes = 'YXS' + else: + self.shape = (image_depth, image_length, image_width, + samples_per_pixel) + self.axes = 'ZYXS' + else: + self._shape = (1, samples_per_pixel, image_depth, + image_length, image_width, 1) + if image_depth == 1: + self.shape = (samples_per_pixel, image_length, image_width) + self.axes = 'SYX' + else: + self.shape = (samples_per_pixel, image_depth, + image_length, image_width) + self.axes = 'SZYX' + if False and self.is_rgb and 'extra_samples' in self.tags: + # DISABLED: only use RGB and first alpha channel if exists + extra_samples = self.extra_samples + if self.tags['extra_samples'].count == 1: + extra_samples = (extra_samples, ) + for exs in extra_samples: + if exs in ('unassalpha', 'assocalpha', 'unspecified'): + if self.is_contig: + self.shape = self.shape[:-1] + (4,) + else: + self.shape = (4,) + self.shape[1:] + break + else: + self._shape = (1, 1, image_depth, image_length, image_width, 1) + if image_depth == 1: + self.shape = (image_length, image_width) + self.axes = 'YX' + else: + self.shape = (image_depth, image_length, image_width) + self.axes = 'ZYX' + if not self.compression and 'strip_byte_counts' not in tags: + self.strip_byte_counts = ( + product(self.shape) * (self.bits_per_sample // 8), ) + + assert len(self.shape) == len(self.axes) + + def asarray(self, squeeze=True, colormapped=True, rgbonly=False, + scale_mdgel=False, memmap=False, reopen=True): + """Read image data from file and return as numpy array. + + Raise ValueError if format is unsupported. + If any of 'squeeze', 'colormapped', or 'rgbonly' are not the default, + the shape of the returned array might be different from the page shape. + + Parameters + ---------- + squeeze : bool + If True, all length-1 dimensions (except X and Y) are + squeezed out from result. + colormapped : bool + If True, color mapping is applied for palette-indexed images. + rgbonly : bool + If True, return RGB(A) image without additional extra samples. + memmap : bool + If True, use numpy.memmap to read arrays from file if possible. + For use on 64 bit systems and files with few huge contiguous data. + reopen : bool + If True and the parent file handle is closed, the file is + temporarily re-opened (and closed if no exception occurs). + scale_mdgel : bool + If True, MD Gel data will be scaled according to the private + metadata in the second TIFF page. The dtype will be float32. + + """ + if not self._shape: + return + + if self.dtype is None: + raise ValueError("data type not supported: %s%i" % ( + self.sample_format, self.bits_per_sample)) + if self.compression not in TIFF_DECOMPESSORS: + raise ValueError("cannot decompress %s" % self.compression) + tag = self.tags['sample_format'] + if tag.count != 1 and any((i-tag.value[0] for i in tag.value)): + raise ValueError("sample formats don't match %s" % str(tag.value)) + + fh = self.parent.filehandle + closed = fh.closed + if closed: + if reopen: + fh.open() + else: + raise IOError("file handle is closed") + + dtype = self._dtype + shape = self._shape + image_width = self.image_width + image_length = self.image_length + image_depth = self.image_depth + typecode = self.parent.byteorder + dtype + bits_per_sample = self.bits_per_sample + + if self.is_tiled: + if 'tile_offsets' in self.tags: + byte_counts = self.tile_byte_counts + offsets = self.tile_offsets + else: + byte_counts = self.strip_byte_counts + offsets = self.strip_offsets + tile_width = self.tile_width + tile_length = self.tile_length + tile_depth = self.tile_depth if 'tile_depth' in self.tags else 1 + tw = (image_width + tile_width - 1) // tile_width + tl = (image_length + tile_length - 1) // tile_length + td = (image_depth + tile_depth - 1) // tile_depth + shape = (shape[0], shape[1], + td*tile_depth, tl*tile_length, tw*tile_width, shape[-1]) + tile_shape = (tile_depth, tile_length, tile_width, shape[-1]) + runlen = tile_width + else: + byte_counts = self.strip_byte_counts + offsets = self.strip_offsets + runlen = image_width + + if any(o < 2 for o in offsets): + raise ValueError("corrupted page") + + if memmap and self._is_memmappable(rgbonly, colormapped): + result = fh.memmap_array(typecode, shape, offset=offsets[0]) + elif self.is_contiguous: + fh.seek(offsets[0]) + result = fh.read_array(typecode, product(shape)) + result = result.astype('=' + dtype) + else: + if self.is_contig: + runlen *= self.samples_per_pixel + if bits_per_sample in (8, 16, 32, 64, 128): + if (bits_per_sample * runlen) % 8: + raise ValueError("data and sample size mismatch") + + def unpack(x): + try: + return numpy.fromstring(x, typecode) + except ValueError as e: + # strips may be missing EOI + warnings.warn("unpack: %s" % e) + xlen = ((len(x) // (bits_per_sample // 8)) + * (bits_per_sample // 8)) + return numpy.fromstring(x[:xlen], typecode) + + elif isinstance(bits_per_sample, tuple): + def unpack(x): + return unpackrgb(x, typecode, bits_per_sample) + else: + def unpack(x): + return unpackints(x, typecode, bits_per_sample, runlen) + + decompress = TIFF_DECOMPESSORS[self.compression] + if self.compression == 'jpeg': + table = self.jpeg_tables if 'jpeg_tables' in self.tags else b'' + decompress = lambda x: decodejpg(x, table, self.photometric) + + if self.is_tiled: + result = numpy.empty(shape, dtype) + tw, tl, td, pl = 0, 0, 0, 0 + for offset, bytecount in zip(offsets, byte_counts): + fh.seek(offset) + tile = unpack(decompress(fh.read(bytecount))) + tile.shape = tile_shape + if self.predictor == 'horizontal': + numpy.cumsum(tile, axis=-2, dtype=dtype, out=tile) + result[0, pl, td:td+tile_depth, + tl:tl+tile_length, tw:tw+tile_width, :] = tile + del tile + tw += tile_width + if tw >= shape[4]: + tw, tl = 0, tl + tile_length + if tl >= shape[3]: + tl, td = 0, td + tile_depth + if td >= shape[2]: + td, pl = 0, pl + 1 + result = result[..., + :image_depth, :image_length, :image_width, :] + else: + strip_size = (self.rows_per_strip * self.image_width * + self.samples_per_pixel) + result = numpy.empty(shape, dtype).reshape(-1) + index = 0 + for offset, bytecount in zip(offsets, byte_counts): + fh.seek(offset) + strip = fh.read(bytecount) + strip = decompress(strip) + strip = unpack(strip) + size = min(result.size, strip.size, strip_size, + result.size - index) + result[index:index+size] = strip[:size] + del strip + index += size + + result.shape = self._shape + + if self.predictor == 'horizontal' and not (self.is_tiled and not + self.is_contiguous): + # work around bug in LSM510 software + if not (self.parent.is_lsm and not self.compression): + numpy.cumsum(result, axis=-2, dtype=dtype, out=result) + + if colormapped and self.is_palette: + if self.color_map.shape[1] >= 2**bits_per_sample: + # FluoView and LSM might fail here + result = numpy.take(self.color_map, + result[:, 0, :, :, :, 0], axis=1) + elif rgbonly and self.is_rgb and 'extra_samples' in self.tags: + # return only RGB and first alpha channel if exists + extra_samples = self.extra_samples + if self.tags['extra_samples'].count == 1: + extra_samples = (extra_samples, ) + for i, exs in enumerate(extra_samples): + if exs in ('unassalpha', 'assocalpha', 'unspecified'): + if self.is_contig: + result = result[..., [0, 1, 2, 3+i]] + else: + result = result[:, [0, 1, 2, 3+i]] + break + else: + if self.is_contig: + result = result[..., :3] + else: + result = result[:, :3] + + if squeeze: + try: + result.shape = self.shape + except ValueError: + warnings.warn("failed to reshape from %s to %s" % ( + str(result.shape), str(self.shape))) + + if scale_mdgel and self.parent.is_mdgel: + # MD Gel stores private metadata in the second page + tags = self.parent.pages[1] + if tags.md_file_tag in (2, 128): + scale = tags.md_scale_pixel + scale = scale[0] / scale[1] # rational + result = result.astype('float32') + if tags.md_file_tag == 2: + result **= 2 # squary root data format + result *= scale + + if closed: + # TODO: file remains open if an exception occurred above + fh.close() + return result + + def _is_memmappable(self, rgbonly, colormapped): + """Return if image data in file can be memory mapped.""" + if not self.parent.filehandle.is_file or not self.is_contiguous: + return False + return not (self.predictor or + (rgbonly and 'extra_samples' in self.tags) or + (colormapped and self.is_palette) or + ({'big': '>', 'little': '<'}[sys.byteorder] != + self.parent.byteorder)) + + @lazyattr + def is_contiguous(self): + """Return offset and size of contiguous data, else None. + + Excludes prediction and colormapping. + + """ + if self.compression or self.bits_per_sample not in (8, 16, 32, 64): + return + if self.is_tiled: + if (self.image_width != self.tile_width or + self.image_length % self.tile_length or + self.tile_width % 16 or self.tile_length % 16): + return + if ('image_depth' in self.tags and 'tile_depth' in self.tags and + (self.image_length != self.tile_length or + self.image_depth % self.tile_depth)): + return + offsets = self.tile_offsets + byte_counts = self.tile_byte_counts + else: + offsets = self.strip_offsets + byte_counts = self.strip_byte_counts + if len(offsets) == 1: + return offsets[0], byte_counts[0] + if self.is_stk or all(offsets[i] + byte_counts[i] == offsets[i+1] + or byte_counts[i+1] == 0 # no data/ignore offset + for i in range(len(offsets)-1)): + return offsets[0], sum(byte_counts) + + def __str__(self): + """Return string containing information about page.""" + s = ', '.join(s for s in ( + ' x '.join(str(i) for i in self.shape), + str(numpy.dtype(self.dtype)), + '%s bit' % str(self.bits_per_sample), + self.photometric if 'photometric' in self.tags else '', + self.compression if self.compression else 'raw', + '|'.join(t[3:] for t in ( + 'is_stk', 'is_lsm', 'is_nih', 'is_ome', 'is_imagej', + 'is_micromanager', 'is_fluoview', 'is_mdgel', 'is_mediacy', + 'is_sgi', 'is_reduced', 'is_tiled', + 'is_contiguous') if getattr(self, t))) if s) + return "Page %i: %s" % (self.index, s) + + def __getattr__(self, name): + """Return tag value.""" + if name in self.tags: + value = self.tags[name].value + setattr(self, name, value) + return value + raise AttributeError(name) + + @lazyattr + def uic_tags(self): + """Consolidate UIC tags.""" + if not self.is_stk: + raise AttributeError("uic_tags") + tags = self.tags + result = Record() + result.number_planes = tags['uic2tag'].count + if 'image_description' in tags: + result.plane_descriptions = self.image_description.split(b'\x00') + if 'uic1tag' in tags: + result.update(tags['uic1tag'].value) + if 'uic3tag' in tags: + result.update(tags['uic3tag'].value) # wavelengths + if 'uic4tag' in tags: + result.update(tags['uic4tag'].value) # override uic1 tags + uic2tag = tags['uic2tag'].value + result.z_distance = uic2tag.z_distance + result.time_created = uic2tag.time_created + result.time_modified = uic2tag.time_modified + try: + result.datetime_created = [ + julian_datetime(*dt) for dt in + zip(uic2tag.date_created, uic2tag.time_created)] + result.datetime_modified = [ + julian_datetime(*dt) for dt in + zip(uic2tag.date_modified, uic2tag.time_modified)] + except ValueError as e: + warnings.warn("uic_tags: %s" % e) + return result + + @lazyattr + def imagej_tags(self): + """Consolidate ImageJ metadata.""" + if not self.is_imagej: + raise AttributeError("imagej_tags") + tags = self.tags + if 'image_description_1' in tags: + # MicroManager + result = imagej_description(tags['image_description_1'].value) + else: + result = imagej_description(tags['image_description'].value) + if 'imagej_metadata' in tags: + try: + result.update(imagej_metadata( + tags['imagej_metadata'].value, + tags['imagej_byte_counts'].value, + self.parent.byteorder)) + except Exception as e: + warnings.warn(str(e)) + return Record(result) + + @lazyattr + def is_rgb(self): + """True if page contains a RGB image.""" + return ('photometric' in self.tags and + self.tags['photometric'].value == 2) + + @lazyattr + def is_contig(self): + """True if page contains a contiguous image.""" + return ('planar_configuration' in self.tags and + self.tags['planar_configuration'].value == 1) + + @lazyattr + def is_palette(self): + """True if page contains a palette-colored image and not OME or STK.""" + try: + # turn off color mapping for OME-TIFF and STK + if self.is_stk or self.is_ome or self.parent.is_ome: + return False + except IndexError: + pass # OME-XML not found in first page + return ('photometric' in self.tags and + self.tags['photometric'].value == 3) + + @lazyattr + def is_tiled(self): + """True if page contains tiled image.""" + return 'tile_width' in self.tags + + @lazyattr + def is_reduced(self): + """True if page is a reduced image of another image.""" + return bool(self.tags['new_subfile_type'].value & 1) + + @lazyattr + def is_mdgel(self): + """True if page contains md_file_tag tag.""" + return 'md_file_tag' in self.tags + + @lazyattr + def is_mediacy(self): + """True if page contains Media Cybernetics Id tag.""" + return ('mc_id' in self.tags and + self.tags['mc_id'].value.startswith(b'MC TIFF')) + + @lazyattr + def is_stk(self): + """True if page contains UIC2Tag tag.""" + return 'uic2tag' in self.tags + + @lazyattr + def is_lsm(self): + """True if page contains LSM CZ_LSM_INFO tag.""" + return 'cz_lsm_info' in self.tags + + @lazyattr + def is_fluoview(self): + """True if page contains FluoView MM_STAMP tag.""" + return 'mm_stamp' in self.tags + + @lazyattr + def is_nih(self): + """True if page contains NIH image header.""" + return 'nih_image_header' in self.tags + + @lazyattr + def is_sgi(self): + """True if page contains SGI image and tile depth tags.""" + return 'image_depth' in self.tags and 'tile_depth' in self.tags + + @lazyattr + def is_ome(self): + """True if page contains OME-XML in image_description tag.""" + return ('image_description' in self.tags and self.tags[ + 'image_description'].value.startswith(b' parent.offset_size or code in CUSTOM_TAGS: + pos = fh.tell() + tof = {4: 'I', 8: 'Q'}[parent.offset_size] + self.value_offset = offset = struct.unpack(byteorder+tof, value)[0] + if offset < 0 or offset > parent.filehandle.size: + raise TiffTag.Error("corrupt file - invalid tag value offset") + elif offset < 4: + raise TiffTag.Error("corrupt value offset for tag %i" % code) + fh.seek(offset) + if code in CUSTOM_TAGS: + readfunc = CUSTOM_TAGS[code][1] + value = readfunc(fh, byteorder, dtype, count) + if isinstance(value, dict): # numpy.core.records.record + value = Record(value) + elif code in TIFF_TAGS or dtype[-1] == 's': + value = struct.unpack(fmt, fh.read(size)) + else: + value = read_numpy(fh, byteorder, dtype, count) + fh.seek(pos) + else: + value = struct.unpack(fmt, value[:size]) + + if code not in CUSTOM_TAGS and code not in (273, 279, 324, 325): + # scalar value if not strip/tile offsets/byte_counts + if len(value) == 1: + value = value[0] + + if (dtype.endswith('s') and isinstance(value, bytes) + and self._type != 7): + # TIFF ASCII fields can contain multiple strings, + # each terminated with a NUL + value = stripascii(value) + + self.code = code + self.name = name + self.dtype = dtype + self.count = count + self.value = value + + def _correct_lsm_bitspersample(self, parent): + """Correct LSM bitspersample tag. + + Old LSM writers may use a separate region for two 16-bit values, + although they fit into the tag value element of the tag. + + """ + if self.code == 258 and self.count == 2: + # TODO: test this. Need example file. + warnings.warn("correcting LSM bitspersample tag") + fh = parent.filehandle + tof = {4: '') + + def __str__(self): + """Return string containing information about tag.""" + return ' '.join(str(getattr(self, s)) for s in self.__slots__) + + +class TiffSequence(object): + """Sequence of image files. + + The data shape and dtype of all files must match. + + Properties + ---------- + files : list + List of file names. + shape : tuple + Shape of image sequence. + axes : str + Labels of axes in shape. + + Examples + -------- + >>> tifs = TiffSequence("test.oif.files/*.tif") + >>> tifs.shape, tifs.axes + ((2, 100), 'CT') + >>> data = tifs.asarray() + >>> data.shape + (2, 100, 256, 256) + + """ + _patterns = { + 'axes': r""" + # matches Olympus OIF and Leica TIFF series + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4})) + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + """} + + class ParseError(Exception): + pass + + def __init__(self, files, imread=TiffFile, pattern='axes', + *args, **kwargs): + """Initialize instance from multiple files. + + Parameters + ---------- + files : str, or sequence of str + Glob pattern or sequence of file names. + imread : function or class + Image read function or class with asarray function returning numpy + array from single file. + pattern : str + Regular expression pattern that matches axes names and sequence + indices in file names. + By default this matches Olympus OIF and Leica TIFF series. + + """ + if isinstance(files, basestring): + files = natural_sorted(glob.glob(files)) + files = list(files) + if not files: + raise ValueError("no files found") + #if not os.path.isfile(files[0]): + # raise ValueError("file not found") + self.files = files + + if hasattr(imread, 'asarray'): + # redefine imread + _imread = imread + + def imread(fname, *args, **kwargs): + with _imread(fname) as im: + return im.asarray(*args, **kwargs) + + self.imread = imread + + self.pattern = self._patterns.get(pattern, pattern) + try: + self._parse() + if not self.axes: + self.axes = 'I' + except self.ParseError: + self.axes = 'I' + self.shape = (len(files),) + self._start_index = (0,) + self._indices = tuple((i,) for i in range(len(files))) + + def __str__(self): + """Return string with information about image sequence.""" + return "\n".join([ + self.files[0], + '* files: %i' % len(self.files), + '* axes: %s' % self.axes, + '* shape: %s' % str(self.shape)]) + + def __len__(self): + return len(self.files) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def close(self): + pass + + def asarray(self, memmap=False, *args, **kwargs): + """Read image data from all files and return as single numpy array. + + If memmap is True, return an array stored in a binary file on disk. + The args and kwargs parameters are passed to the imread function. + + Raise IndexError or ValueError if image shapes don't match. + + """ + im = self.imread(self.files[0], *args, **kwargs) + shape = self.shape + im.shape + if memmap: + with tempfile.NamedTemporaryFile() as fh: + result = numpy.memmap(fh, dtype=im.dtype, shape=shape) + else: + result = numpy.zeros(shape, dtype=im.dtype) + result = result.reshape(-1, *im.shape) + for index, fname in zip(self._indices, self.files): + index = [i-j for i, j in zip(index, self._start_index)] + index = numpy.ravel_multi_index(index, self.shape) + im = self.imread(fname, *args, **kwargs) + result[index] = im + result.shape = shape + return result + + def _parse(self): + """Get axes and shape from file names.""" + if not self.pattern: + raise self.ParseError("invalid pattern") + pattern = re.compile(self.pattern, re.IGNORECASE | re.VERBOSE) + matches = pattern.findall(self.files[0]) + if not matches: + raise self.ParseError("pattern doesn't match file names") + matches = matches[-1] + if len(matches) % 2: + raise self.ParseError("pattern doesn't match axis name and index") + axes = ''.join(m for m in matches[::2] if m) + if not axes: + raise self.ParseError("pattern doesn't match file names") + + indices = [] + for fname in self.files: + matches = pattern.findall(fname)[-1] + if axes != ''.join(m for m in matches[::2] if m): + raise ValueError("axes don't match within the image sequence") + indices.append([int(m) for m in matches[1::2] if m]) + shape = tuple(numpy.max(indices, axis=0)) + start_index = tuple(numpy.min(indices, axis=0)) + shape = tuple(i-j+1 for i, j in zip(shape, start_index)) + if product(shape) != len(self.files): + warnings.warn("files are missing. Missing data are zeroed") + + self.axes = axes.upper() + self.shape = shape + self._indices = indices + self._start_index = start_index + + +class Record(dict): + """Dictionary with attribute access. + + Can also be initialized with numpy.core.records.record. + + """ + __slots__ = () + + def __init__(self, arg=None, **kwargs): + if kwargs: + arg = kwargs + elif arg is None: + arg = {} + try: + dict.__init__(self, arg) + except (TypeError, ValueError): + for i, name in enumerate(arg.dtype.names): + v = arg[i] + self[name] = v if v.dtype.char != 'S' else stripnull(v) + + def __getattr__(self, name): + return self[name] + + def __setattr__(self, name, value): + self.__setitem__(name, value) + + def __str__(self): + """Pretty print Record.""" + s = [] + lists = [] + for k in sorted(self): + try: + if k.startswith('_'): # does not work with byte + continue + except AttributeError: + pass + v = self[k] + if isinstance(v, (list, tuple)) and len(v): + if isinstance(v[0], Record): + lists.append((k, v)) + continue + elif isinstance(v[0], TiffPage): + v = [i.index for i in v if i] + s.append( + ("* %s: %s" % (k, str(v))).split("\n", 1)[0] + [:PRINT_LINE_LEN].rstrip()) + for k, v in lists: + l = [] + for i, w in enumerate(v): + l.append("* %s[%i]\n %s" % (k, i, + str(w).replace("\n", "\n "))) + s.append('\n'.join(l)) + return '\n'.join(s) + + +class TiffTags(Record): + """Dictionary of TiffTag with attribute access.""" + + def __str__(self): + """Return string with information about all tags.""" + s = [] + for tag in sorted(self.values(), key=lambda x: x.code): + typecode = "%i%s" % (tag.count * int(tag.dtype[0]), tag.dtype[1]) + line = "* %i %s (%s) %s" % ( + tag.code, tag.name, typecode, tag.as_str()) + s.append(line[:PRINT_LINE_LEN].lstrip()) + return '\n'.join(s) + + +class FileHandle(object): + """Binary file handle. + + * Handle embedded files (for CZI within CZI files). + * Allow to re-open closed files (for multi file formats such as OME-TIFF). + * Read numpy arrays and records from file like objects. + + Only binary read, seek, tell, and close are supported on embedded files. + When initialized from another file handle, do not use it unless this + FileHandle is closed. + + Attributes + ---------- + name : str + Name of the file. + path : str + Absolute path to file. + size : int + Size of file in bytes. + is_file : bool + If True, file has a filno and can be memory mapped. + + All attributes are read-only. + + """ + __slots__ = ('_fh', '_arg', '_mode', '_name', '_dir', + '_offset', '_size', '_close', 'is_file') + + def __init__(self, arg, mode='rb', name=None, offset=None, size=None): + """Initialize file handle from file name or another file handle. + + Parameters + ---------- + arg : str, File, or FileHandle + File name or open file handle. + mode : str + File open mode in case 'arg' is a file name. + name : str + Optional name of file in case 'arg' is a file handle. + offset : int + Optional start position of embedded file. By default this is + the current file position. + size : int + Optional size of embedded file. By default this is the number + of bytes from the 'offset' to the end of the file. + + """ + self._fh = None + self._arg = arg + self._mode = mode + self._name = name + self._dir = '' + self._offset = offset + self._size = size + self._close = True + self.is_file = False + self.open() + + def open(self): + """Open or re-open file.""" + if self._fh: + return # file is open + + if isinstance(self._arg, basestring): + # file name + self._arg = os.path.abspath(self._arg) + self._dir, self._name = os.path.split(self._arg) + self._fh = open(self._arg, self._mode) + self._close = True + if self._offset is None: + self._offset = 0 + elif isinstance(self._arg, FileHandle): + # FileHandle + self._fh = self._arg._fh + if self._offset is None: + self._offset = 0 + self._offset += self._arg._offset + self._close = False + if not self._name: + if self._offset: + name, ext = os.path.splitext(self._arg._name) + self._name = "%s@%i%s" % (name, self._offset, ext) + else: + self._name = self._arg._name + self._dir = self._arg._dir + else: + # open file object + self._fh = self._arg + if self._offset is None: + self._offset = self._arg.tell() + self._close = False + if not self._name: + try: + self._dir, self._name = os.path.split(self._fh.name) + except AttributeError: + self._name = "Unnamed stream" + + if self._offset: + self._fh.seek(self._offset) + + if self._size is None: + pos = self._fh.tell() + self._fh.seek(self._offset, 2) + self._size = self._fh.tell() + self._fh.seek(pos) + + try: + self._fh.fileno() + self.is_file = True + except Exception: + self.is_file = False + + def read(self, size=-1): + """Read 'size' bytes from file, or until EOF is reached.""" + if size < 0 and self._offset: + size = self._size + return self._fh.read(size) + + def memmap_array(self, dtype, shape, offset=0, mode='r', order='C'): + """Return numpy.memmap of data stored in file.""" + if not self.is_file: + raise ValueError("Can not memory map file without fileno.") + return numpy.memmap(self._fh, dtype=dtype, mode=mode, + offset=self._offset + offset, + shape=shape, order=order) + + def read_array(self, dtype, count=-1, sep=""): + """Return numpy array from file. + + Work around numpy issue #2230, "numpy.fromfile does not accept + StringIO object" https://github.com/numpy/numpy/issues/2230. + + """ + try: + return numpy.fromfile(self._fh, dtype, count, sep) + except IOError: + if count < 0: + size = self._size + else: + size = count * numpy.dtype(dtype).itemsize + data = self._fh.read(size) + return numpy.fromstring(data, dtype, count, sep) + + def read_record(self, dtype, shape=1, byteorder=None): + """Return numpy record from file.""" + try: + rec = numpy.rec.fromfile(self._fh, dtype, shape, + byteorder=byteorder) + except Exception: + dtype = numpy.dtype(dtype) + if shape is None: + shape = self._size // dtype.itemsize + size = product(sequence(shape)) * dtype.itemsize + data = self._fh.read(size) + return numpy.rec.fromstring(data, dtype, shape, + byteorder=byteorder) + return rec[0] if shape == 1 else rec + + def tell(self): + """Return file's current position.""" + return self._fh.tell() - self._offset + + def seek(self, offset, whence=0): + """Set file's current position.""" + if self._offset: + if whence == 0: + self._fh.seek(self._offset + offset, whence) + return + elif whence == 2: + self._fh.seek(self._offset + self._size + offset, 0) + return + self._fh.seek(offset, whence) + + def close(self): + """Close file.""" + if self._close and self._fh: + self._fh.close() + self._fh = None + self.is_file = False + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def __getattr__(self, name): + """Return attribute from underlying file object.""" + if self._offset: + warnings.warn( + "FileHandle: '%s' not implemented for embedded files" % name) + return getattr(self._fh, name) + + @property + def name(self): + return self._name + + @property + def dirname(self): + return self._dir + + @property + def path(self): + return os.path.join(self._dir, self._name) + + @property + def size(self): + return self._size + + @property + def closed(self): + return self._fh is None + + +def read_bytes(fh, byteorder, dtype, count): + """Read tag data from file and return as byte string.""" + dtype = 'b' if dtype[-1] == 's' else byteorder+dtype[-1] + return fh.read_array(dtype, count).tostring() + + +def read_numpy(fh, byteorder, dtype, count): + """Read tag data from file and return as numpy array.""" + dtype = 'b' if dtype[-1] == 's' else byteorder+dtype[-1] + return fh.read_array(dtype, count) + + +def read_json(fh, byteorder, dtype, count): + """Read JSON tag data from file and return as object.""" + data = fh.read(count) + try: + return json.loads(unicode(stripnull(data), 'utf-8')) + except ValueError: + warnings.warn("invalid JSON `%s`" % data) + + +def read_mm_header(fh, byteorder, dtype, count): + """Read MM_HEADER tag from file and return as numpy.rec.array.""" + return fh.read_record(MM_HEADER, byteorder=byteorder) + + +def read_mm_stamp(fh, byteorder, dtype, count): + """Read MM_STAMP tag from file and return as numpy.array.""" + return fh.read_array(byteorder+'f8', 8) + + +def read_uic1tag(fh, byteorder, dtype, count, plane_count=None): + """Read MetaMorph STK UIC1Tag from file and return as dictionary. + + Return empty dictionary if plane_count is unknown. + + """ + assert dtype in ('2I', '1I') and byteorder == '<' + result = {} + if dtype == '2I': + # pre MetaMorph 2.5 (not tested) + values = fh.read_array(' structure_size: + break + cz_lsm_info.append((name, dtype)) + else: + cz_lsm_info = CZ_LSM_INFO + + return fh.read_record(cz_lsm_info, byteorder=byteorder) + + +def read_cz_lsm_floatpairs(fh): + """Read LSM sequence of float pairs from file and return as list.""" + size = struct.unpack(' 0: + esize, etime, etype = struct.unpack(''}[fh.read(2)] + except IndexError: + raise ValueError("not a MicroManager TIFF file") + + results = {} + fh.seek(8) + (index_header, index_offset, display_header, display_offset, + comments_header, comments_offset, summary_header, summary_length + ) = struct.unpack(byteorder + "IIIIIIII", fh.read(32)) + + if summary_header != 2355492: + raise ValueError("invalid MicroManager summary_header") + results['summary'] = read_json(fh, byteorder, None, summary_length) + + if index_header != 54773648: + raise ValueError("invalid MicroManager index_header") + fh.seek(index_offset) + header, count = struct.unpack(byteorder + "II", fh.read(8)) + if header != 3453623: + raise ValueError("invalid MicroManager index_header") + data = struct.unpack(byteorder + "IIIII"*count, fh.read(20*count)) + results['index_map'] = { + 'channel': data[::5], 'slice': data[1::5], 'frame': data[2::5], + 'position': data[3::5], 'offset': data[4::5]} + + if display_header != 483765892: + raise ValueError("invalid MicroManager display_header") + fh.seek(display_offset) + header, count = struct.unpack(byteorder + "II", fh.read(8)) + if header != 347834724: + raise ValueError("invalid MicroManager display_header") + results['display_settings'] = read_json(fh, byteorder, None, count) + + if comments_header != 99384722: + raise ValueError("invalid MicroManager comments_header") + fh.seek(comments_offset) + header, count = struct.unpack(byteorder + "II", fh.read(8)) + if header != 84720485: + raise ValueError("invalid MicroManager comments_header") + results['comments'] = read_json(fh, byteorder, None, count) + + return results + + +def imagej_metadata(data, bytecounts, byteorder): + """Return dict from ImageJ metadata tag value.""" + _str = str if sys.version_info[0] < 3 else lambda x: str(x, 'cp1252') + + def read_string(data, byteorder): + return _str(stripnull(data[0 if byteorder == '<' else 1::2])) + + def read_double(data, byteorder): + return struct.unpack(byteorder+('d' * (len(data) // 8)), data) + + def read_bytes(data, byteorder): + #return struct.unpack('b' * len(data), data) + return numpy.fromstring(data, 'uint8') + + metadata_types = { # big endian + b'info': ('info', read_string), + b'labl': ('labels', read_string), + b'rang': ('ranges', read_double), + b'luts': ('luts', read_bytes), + b'roi ': ('roi', read_bytes), + b'over': ('overlays', read_bytes)} + metadata_types.update( # little endian + dict((k[::-1], v) for k, v in metadata_types.items())) + + if not bytecounts: + raise ValueError("no ImageJ metadata") + + if not data[:4] in (b'IJIJ', b'JIJI'): + raise ValueError("invalid ImageJ metadata") + + header_size = bytecounts[0] + if header_size < 12 or header_size > 804: + raise ValueError("invalid ImageJ metadata header size") + + ntypes = (header_size - 4) // 8 + header = struct.unpack(byteorder+'4sI'*ntypes, data[4:4+ntypes*8]) + pos = 4 + ntypes * 8 + counter = 0 + result = {} + for mtype, count in zip(header[::2], header[1::2]): + values = [] + name, func = metadata_types.get(mtype, (_str(mtype), read_bytes)) + for _ in range(count): + counter += 1 + pos1 = pos + bytecounts[counter] + values.append(func(data[pos:pos1], byteorder)) + pos = pos1 + result[name.strip()] = values[0] if count == 1 else values + return result + + +def imagej_description(description): + """Return dict from ImageJ image_description tag.""" + def _bool(val): + return {b'true': True, b'false': False}[val.lower()] + + _str = str if sys.version_info[0] < 3 else lambda x: str(x, 'cp1252') + result = {} + for line in description.splitlines(): + try: + key, val = line.split(b'=') + except Exception: + continue + key = key.strip() + val = val.strip() + for dtype in (int, float, _bool, _str): + try: + val = dtype(val) + break + except Exception: + pass + result[_str(key)] = val + return result + + +def _replace_by(module_function, package=None, warn=False): + """Try replace decorated function by module.function.""" + try: + from importlib import import_module + except ImportError: + warnings.warn('could not import module importlib') + return lambda func: func + + def decorate(func, module_function=module_function, warn=warn): + try: + module, function = module_function.split('.') + if not package: + module = import_module(module) + else: + module = import_module('.' + module, package=package) + func, oldfunc = getattr(module, function), func + globals()['__old_' + func.__name__] = oldfunc + except Exception: + if warn: + warnings.warn("failed to import %s" % module_function) + return func + + return decorate + + +def decodejpg(encoded, tables=b'', photometric=None, + ycbcr_subsampling=None, ycbcr_positioning=None): + """Decode JPEG encoded byte string (using _czifile extension module).""" + import _czifile + image = _czifile.decodejpg(encoded, tables) + if photometric == 'rgb' and ycbcr_subsampling and ycbcr_positioning: + # TODO: convert YCbCr to RGB + pass + return image.tostring() + + +@_replace_by('_tifffile.decodepackbits') +def decodepackbits(encoded): + """Decompress PackBits encoded byte string. + + PackBits is a simple byte-oriented run-length compression scheme. + + """ + func = ord if sys.version[0] == '2' else lambda x: x + result = [] + result_extend = result.extend + i = 0 + try: + while True: + n = func(encoded[i]) + 1 + i += 1 + if n < 129: + result_extend(encoded[i:i+n]) + i += n + elif n > 129: + result_extend(encoded[i:i+1] * (258-n)) + i += 1 + except IndexError: + pass + return b''.join(result) if sys.version[0] == '2' else bytes(result) + + +@_replace_by('_tifffile.decodelzw') +def decodelzw(encoded): + """Decompress LZW (Lempel-Ziv-Welch) encoded TIFF strip (byte string). + + The strip must begin with a CLEAR code and end with an EOI code. + + This is an implementation of the LZW decoding algorithm described in (1). + It is not compatible with old style LZW compressed files like quad-lzw.tif. + + """ + len_encoded = len(encoded) + bitcount_max = len_encoded * 8 + unpack = struct.unpack + + if sys.version[0] == '2': + newtable = [chr(i) for i in range(256)] + else: + newtable = [bytes([i]) for i in range(256)] + newtable.extend((0, 0)) + + def next_code(): + """Return integer of `bitw` bits at `bitcount` position in encoded.""" + start = bitcount // 8 + s = encoded[start:start+4] + try: + code = unpack('>I', s)[0] + except Exception: + code = unpack('>I', s + b'\x00'*(4-len(s)))[0] + code <<= bitcount % 8 + code &= mask + return code >> shr + + switchbitch = { # code: bit-width, shr-bits, bit-mask + 255: (9, 23, int(9*'1'+'0'*23, 2)), + 511: (10, 22, int(10*'1'+'0'*22, 2)), + 1023: (11, 21, int(11*'1'+'0'*21, 2)), + 2047: (12, 20, int(12*'1'+'0'*20, 2)), } + bitw, shr, mask = switchbitch[255] + bitcount = 0 + + if len_encoded < 4: + raise ValueError("strip must be at least 4 characters long") + + if next_code() != 256: + raise ValueError("strip must begin with CLEAR code") + + code = 0 + oldcode = 0 + result = [] + result_append = result.append + while True: + code = next_code() # ~5% faster when inlining this function + bitcount += bitw + if code == 257 or bitcount >= bitcount_max: # EOI + break + if code == 256: # CLEAR + table = newtable[:] + table_append = table.append + lentable = 258 + bitw, shr, mask = switchbitch[255] + code = next_code() + bitcount += bitw + if code == 257: # EOI + break + result_append(table[code]) + else: + if code < lentable: + decoded = table[code] + newcode = table[oldcode] + decoded[:1] + else: + newcode = table[oldcode] + newcode += newcode[:1] + decoded = newcode + result_append(decoded) + table_append(newcode) + lentable += 1 + oldcode = code + if lentable in switchbitch: + bitw, shr, mask = switchbitch[lentable] + + if code != 257: + warnings.warn("unexpected end of lzw stream (code %i)" % code) + + return b''.join(result) + + +@_replace_by('_tifffile.unpackints') +def unpackints(data, dtype, itemsize, runlen=0): + """Decompress byte string to array of integers of any bit size <= 32. + + Parameters + ---------- + data : byte str + Data to decompress. + dtype : numpy.dtype or str + A numpy boolean or integer type. + itemsize : int + Number of bits per integer. + runlen : int + Number of consecutive integers, after which to start at next byte. + + """ + if itemsize == 1: # bitarray + data = numpy.fromstring(data, '|B') + data = numpy.unpackbits(data) + if runlen % 8: + data = data.reshape(-1, runlen + (8 - runlen % 8)) + data = data[:, :runlen].reshape(-1) + return data.astype(dtype) + + dtype = numpy.dtype(dtype) + if itemsize in (8, 16, 32, 64): + return numpy.fromstring(data, dtype) + if itemsize < 1 or itemsize > 32: + raise ValueError("itemsize out of range: %i" % itemsize) + if dtype.kind not in "biu": + raise ValueError("invalid dtype") + + itembytes = next(i for i in (1, 2, 4, 8) if 8 * i >= itemsize) + if itembytes != dtype.itemsize: + raise ValueError("dtype.itemsize too small") + if runlen == 0: + runlen = len(data) // itembytes + skipbits = runlen*itemsize % 8 + if skipbits: + skipbits = 8 - skipbits + shrbits = itembytes*8 - itemsize + bitmask = int(itemsize*'1'+'0'*shrbits, 2) + dtypestr = '>' + dtype.char # dtype always big endian? + + unpack = struct.unpack + l = runlen * (len(data)*8 // (runlen*itemsize + skipbits)) + result = numpy.empty((l, ), dtype) + bitcount = 0 + for i in range(len(result)): + start = bitcount // 8 + s = data[start:start+itembytes] + try: + code = unpack(dtypestr, s)[0] + except Exception: + code = unpack(dtypestr, s + b'\x00'*(itembytes-len(s)))[0] + code <<= bitcount % 8 + code &= bitmask + result[i] = code >> shrbits + bitcount += itemsize + if (i+1) % runlen == 0: + bitcount += skipbits + return result + + +def unpackrgb(data, dtype='>> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff) + >>> print(unpackrgb(data, '>> print(unpackrgb(data, '>> print(unpackrgb(data, '= bits) + data = numpy.fromstring(data, dtype.byteorder+dt) + result = numpy.empty((data.size, len(bitspersample)), dtype.char) + for i, bps in enumerate(bitspersample): + t = data >> int(numpy.sum(bitspersample[i+1:])) + t &= int('0b'+'1'*bps, 2) + if rescale: + o = ((dtype.itemsize * 8) // bps + 1) * bps + if o > data.dtype.itemsize * 8: + t = t.astype('I') + t *= (2**o - 1) // (2**bps - 1) + t //= 2**(o - (dtype.itemsize * 8)) + result[:, i] = t + return result.reshape(-1) + + +def reorient(image, orientation): + """Return reoriented view of image array. + + Parameters + ---------- + image : numpy array + Non-squeezed output of asarray() functions. + Axes -3 and -2 must be image length and width respectively. + orientation : int or str + One of TIFF_ORIENTATIONS keys or values. + + """ + o = TIFF_ORIENTATIONS.get(orientation, orientation) + if o == 'top_left': + return image + elif o == 'top_right': + return image[..., ::-1, :] + elif o == 'bottom_left': + return image[..., ::-1, :, :] + elif o == 'bottom_right': + return image[..., ::-1, ::-1, :] + elif o == 'left_top': + return numpy.swapaxes(image, -3, -2) + elif o == 'right_top': + return numpy.swapaxes(image, -3, -2)[..., ::-1, :] + elif o == 'left_bottom': + return numpy.swapaxes(image, -3, -2)[..., ::-1, :, :] + elif o == 'right_bottom': + return numpy.swapaxes(image, -3, -2)[..., ::-1, ::-1, :] + + +def squeeze_axes(shape, axes, skip='XY'): + """Return shape and axes with single-dimensional entries removed. + + Remove unused dimensions unless their axes are listed in 'skip'. + + >>> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC') + ((5, 2, 1), 'TYX') + + """ + if len(shape) != len(axes): + raise ValueError("dimensions of axes and shape don't match") + shape, axes = zip(*(i for i in zip(shape, axes) + if i[0] > 1 or i[1] in skip)) + return shape, ''.join(axes) + + +def transpose_axes(data, axes, asaxes='CTZYX'): + """Return data with its axes permuted to match specified axes. + + A view is returned if possible. + + >>> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape + (5, 2, 1, 3, 4) + + """ + for ax in axes: + if ax not in asaxes: + raise ValueError("unknown axis %s" % ax) + # add missing axes to data + shape = data.shape + for ax in reversed(asaxes): + if ax not in axes: + axes = ax + axes + shape = (1,) + shape + data = data.reshape(shape) + # transpose axes + data = data.transpose([axes.index(ax) for ax in asaxes]) + return data + + +def stack_pages(pages, memmap=False, *args, **kwargs): + """Read data from sequence of TiffPage and stack them vertically. + + If memmap is True, return an array stored in a binary file on disk. + Additional parameters are passsed to the page asarray function. + + """ + if len(pages) == 0: + raise ValueError("no pages") + + if len(pages) == 1: + return pages[0].asarray(memmap=memmap, *args, **kwargs) + + result = pages[0].asarray(*args, **kwargs) + shape = (len(pages),) + result.shape + if memmap: + with tempfile.NamedTemporaryFile() as fh: + result = numpy.memmap(fh, dtype=result.dtype, shape=shape) + else: + result = numpy.empty(shape, dtype=result.dtype) + + for i, page in enumerate(pages): + result[i] = page.asarray(*args, **kwargs) + + return result + + +def stripnull(string): + """Return string truncated at first null character. + + Clean NULL terminated C strings. + + >>> stripnull(b'string\\x00') + b'string' + + """ + i = string.find(b'\x00') + return string if (i < 0) else string[:i] + + +def stripascii(string): + """Return string truncated at last byte that is 7bit ASCII. + + Clean NULL separated and terminated TIFF strings. + + >>> stripascii(b'string\\x00string\\n\\x01\\x00') + b'string\\x00string\\n' + >>> stripascii(b'\\x00') + b'' + + """ + # TODO: pythonize this + ord_ = ord if sys.version_info[0] < 3 else lambda x: x + i = len(string) + while i: + i -= 1 + if 8 < ord_(string[i]) < 127: + break + else: + i = -1 + return string[:i+1] + + +def format_size(size): + """Return file size as string from byte size.""" + for unit in ('B', 'KB', 'MB', 'GB', 'TB'): + if size < 2048: + return "%.f %s" % (size, unit) + size /= 1024.0 + + +def sequence(value): + """Return tuple containing value if value is not a sequence. + + >>> sequence(1) + (1,) + >>> sequence([1]) + [1] + + """ + try: + len(value) + return value + except TypeError: + return (value, ) + + +def product(iterable): + """Return product of sequence of numbers. + + Equivalent of functools.reduce(operator.mul, iterable, 1). + + >>> product([2**8, 2**30]) + 274877906944 + >>> product([]) + 1 + + """ + prod = 1 + for i in iterable: + prod *= i + return prod + + +def natural_sorted(iterable): + """Return human sorted list of strings. + + E.g. for sorting file names. + + >>> natural_sorted(['f1', 'f2', 'f10']) + ['f1', 'f2', 'f10'] + + """ + def sortkey(x): + return [(int(c) if c.isdigit() else c) for c in re.split(numbers, x)] + numbers = re.compile(r'(\d+)') + return sorted(iterable, key=sortkey) + + +def excel_datetime(timestamp, epoch=datetime.datetime.fromordinal(693594)): + """Return datetime object from timestamp in Excel serial format. + + Convert LSM time stamps. + + >>> excel_datetime(40237.029999999795) + datetime.datetime(2010, 2, 28, 0, 43, 11, 999982) + + """ + return epoch + datetime.timedelta(timestamp) + + +def julian_datetime(julianday, milisecond=0): + """Return datetime from days since 1/1/4713 BC and ms since midnight. + + Convert Julian dates according to MetaMorph. + + >>> julian_datetime(2451576, 54362783) + datetime.datetime(2000, 2, 2, 15, 6, 2, 783) + + """ + if julianday <= 1721423: + # no datetime before year 1 + return None + + a = julianday + 1 + if a > 2299160: + alpha = math.trunc((a - 1867216.25) / 36524.25) + a += 1 + alpha - alpha // 4 + b = a + (1524 if a > 1721423 else 1158) + c = math.trunc((b - 122.1) / 365.25) + d = math.trunc(365.25 * c) + e = math.trunc((b - d) / 30.6001) + + day = b - d - math.trunc(30.6001 * e) + month = e - (1 if e < 13.5 else 13) + year = c - (4716 if month > 2.5 else 4715) + + hour, milisecond = divmod(milisecond, 1000 * 60 * 60) + minute, milisecond = divmod(milisecond, 1000 * 60) + second, milisecond = divmod(milisecond, 1000) + + return datetime.datetime(year, month, day, + hour, minute, second, milisecond) + + +def test_tifffile(directory='testimages', verbose=True): + """Read all images in directory. + + Print error message on failure. + + >>> test_tifffile(verbose=False) + + """ + successful = 0 + failed = 0 + start = time.time() + for f in glob.glob(os.path.join(directory, '*.*')): + if verbose: + print("\n%s>\n" % f.lower(), end='') + t0 = time.time() + try: + tif = TiffFile(f, multifile=True) + except Exception as e: + if not verbose: + print(f, end=' ') + print("ERROR:", e) + failed += 1 + continue + try: + img = tif.asarray() + except ValueError: + try: + img = tif[0].asarray() + except Exception as e: + if not verbose: + print(f, end=' ') + print("ERROR:", e) + failed += 1 + continue + finally: + tif.close() + successful += 1 + if verbose: + print("%s, %s %s, %s, %.0f ms" % ( + str(tif), str(img.shape), img.dtype, tif[0].compression, + (time.time()-t0) * 1e3)) + if verbose: + print("\nSuccessfully read %i of %i files in %.3f s\n" % ( + successful, successful+failed, time.time()-start)) + + +class TIFF_SUBFILE_TYPES(object): + def __getitem__(self, key): + result = [] + if key & 1: + result.append('reduced_image') + if key & 2: + result.append('page') + if key & 4: + result.append('mask') + return tuple(result) + + +TIFF_PHOTOMETRICS = { + 0: 'miniswhite', + 1: 'minisblack', + 2: 'rgb', + 3: 'palette', + 4: 'mask', + 5: 'separated', # CMYK + 6: 'ycbcr', + 8: 'cielab', + 9: 'icclab', + 10: 'itulab', + 32803: 'cfa', # Color Filter Array + 32844: 'logl', + 32845: 'logluv', + 34892: 'linear_raw' +} + +TIFF_COMPESSIONS = { + 1: None, + 2: 'ccittrle', + 3: 'ccittfax3', + 4: 'ccittfax4', + 5: 'lzw', + 6: 'ojpeg', + 7: 'jpeg', + 8: 'adobe_deflate', + 9: 't85', + 10: 't43', + 32766: 'next', + 32771: 'ccittrlew', + 32773: 'packbits', + 32809: 'thunderscan', + 32895: 'it8ctpad', + 32896: 'it8lw', + 32897: 'it8mp', + 32898: 'it8bl', + 32908: 'pixarfilm', + 32909: 'pixarlog', + 32946: 'deflate', + 32947: 'dcs', + 34661: 'jbig', + 34676: 'sgilog', + 34677: 'sgilog24', + 34712: 'jp2000', + 34713: 'nef', +} + +TIFF_DECOMPESSORS = { + None: lambda x: x, + 'adobe_deflate': zlib.decompress, + 'deflate': zlib.decompress, + 'packbits': decodepackbits, + 'lzw': decodelzw, + # 'jpeg': decodejpg +} + +TIFF_DATA_TYPES = { + 1: '1B', # BYTE 8-bit unsigned integer. + 2: '1s', # ASCII 8-bit byte that contains a 7-bit ASCII code; + # the last byte must be NULL (binary zero). + 3: '1H', # SHORT 16-bit (2-byte) unsigned integer + 4: '1I', # LONG 32-bit (4-byte) unsigned integer. + 5: '2I', # RATIONAL Two LONGs: the first represents the numerator of + # a fraction; the second, the denominator. + 6: '1b', # SBYTE An 8-bit signed (twos-complement) integer. + 7: '1s', # UNDEFINED An 8-bit byte that may contain anything, + # depending on the definition of the field. + 8: '1h', # SSHORT A 16-bit (2-byte) signed (twos-complement) integer. + 9: '1i', # SLONG A 32-bit (4-byte) signed (twos-complement) integer. + 10: '2i', # SRATIONAL Two SLONGs: the first represents the numerator + # of a fraction, the second the denominator. + 11: '1f', # FLOAT Single precision (4-byte) IEEE format. + 12: '1d', # DOUBLE Double precision (8-byte) IEEE format. + 13: '1I', # IFD unsigned 4 byte IFD offset. + #14: '', # UNICODE + #15: '', # COMPLEX + 16: '1Q', # LONG8 unsigned 8 byte integer (BigTiff) + 17: '1q', # SLONG8 signed 8 byte integer (BigTiff) + 18: '1Q', # IFD8 unsigned 8 byte IFD offset (BigTiff) +} + +TIFF_SAMPLE_FORMATS = { + 1: 'uint', + 2: 'int', + 3: 'float', + #4: 'void', + #5: 'complex_int', + 6: 'complex', +} + +TIFF_SAMPLE_DTYPES = { + ('uint', 1): '?', # bitmap + ('uint', 2): 'B', + ('uint', 3): 'B', + ('uint', 4): 'B', + ('uint', 5): 'B', + ('uint', 6): 'B', + ('uint', 7): 'B', + ('uint', 8): 'B', + ('uint', 9): 'H', + ('uint', 10): 'H', + ('uint', 11): 'H', + ('uint', 12): 'H', + ('uint', 13): 'H', + ('uint', 14): 'H', + ('uint', 15): 'H', + ('uint', 16): 'H', + ('uint', 17): 'I', + ('uint', 18): 'I', + ('uint', 19): 'I', + ('uint', 20): 'I', + ('uint', 21): 'I', + ('uint', 22): 'I', + ('uint', 23): 'I', + ('uint', 24): 'I', + ('uint', 25): 'I', + ('uint', 26): 'I', + ('uint', 27): 'I', + ('uint', 28): 'I', + ('uint', 29): 'I', + ('uint', 30): 'I', + ('uint', 31): 'I', + ('uint', 32): 'I', + ('uint', 64): 'Q', + ('int', 8): 'b', + ('int', 16): 'h', + ('int', 32): 'i', + ('int', 64): 'q', + ('float', 16): 'e', + ('float', 32): 'f', + ('float', 64): 'd', + ('complex', 64): 'F', + ('complex', 128): 'D', + ('uint', (5, 6, 5)): 'B', +} + +TIFF_ORIENTATIONS = { + 1: 'top_left', + 2: 'top_right', + 3: 'bottom_right', + 4: 'bottom_left', + 5: 'left_top', + 6: 'right_top', + 7: 'right_bottom', + 8: 'left_bottom', +} + +# TODO: is there a standard for character axes labels? +AXES_LABELS = { + 'X': 'width', + 'Y': 'height', + 'Z': 'depth', + 'S': 'sample', # rgb(a) + 'I': 'series', # general sequence, plane, page, IFD + 'T': 'time', + 'C': 'channel', # color, emission wavelength + 'A': 'angle', + 'P': 'phase', # formerly F # P is Position in LSM! + 'R': 'tile', # region, point, mosaic + 'H': 'lifetime', # histogram + 'E': 'lambda', # excitation wavelength + 'L': 'exposure', # lux + 'V': 'event', + 'Q': 'other', + #'M': 'mosaic', # LSM 6 +} + +AXES_LABELS.update(dict((v, k) for k, v in AXES_LABELS.items())) + +# Map OME pixel types to numpy dtype +OME_PIXEL_TYPES = { + 'int8': 'i1', + 'int16': 'i2', + 'int32': 'i4', + 'uint8': 'u1', + 'uint16': 'u2', + 'uint32': 'u4', + 'float': 'f4', + # 'bit': 'bit', + 'double': 'f8', + 'complex': 'c8', + 'double-complex': 'c16', +} + +# NIH Image PicHeader v1.63 +NIH_IMAGE_HEADER = [ + ('fileid', 'a8'), + ('nlines', 'i2'), + ('pixelsperline', 'i2'), + ('version', 'i2'), + ('oldlutmode', 'i2'), + ('oldncolors', 'i2'), + ('colors', 'u1', (3, 32)), + ('oldcolorstart', 'i2'), + ('colorwidth', 'i2'), + ('extracolors', 'u2', (6, 3)), + ('nextracolors', 'i2'), + ('foregroundindex', 'i2'), + ('backgroundindex', 'i2'), + ('xscale', 'f8'), + ('_x0', 'i2'), + ('_x1', 'i2'), + ('units_t', 'i2'), # NIH_UNITS_TYPE + ('p1', [('x', 'i2'), ('y', 'i2')]), + ('p2', [('x', 'i2'), ('y', 'i2')]), + ('curvefit_t', 'i2'), # NIH_CURVEFIT_TYPE + ('ncoefficients', 'i2'), + ('coeff', 'f8', 6), + ('_um_len', 'u1'), + ('um', 'a15'), + ('_x2', 'u1'), + ('binarypic', 'b1'), + ('slicestart', 'i2'), + ('sliceend', 'i2'), + ('scalemagnification', 'f4'), + ('nslices', 'i2'), + ('slicespacing', 'f4'), + ('currentslice', 'i2'), + ('frameinterval', 'f4'), + ('pixelaspectratio', 'f4'), + ('colorstart', 'i2'), + ('colorend', 'i2'), + ('ncolors', 'i2'), + ('fill1', '3u2'), + ('fill2', '3u2'), + ('colortable_t', 'u1'), # NIH_COLORTABLE_TYPE + ('lutmode_t', 'u1'), # NIH_LUTMODE_TYPE + ('invertedtable', 'b1'), + ('zeroclip', 'b1'), + ('_xunit_len', 'u1'), + ('xunit', 'a11'), + ('stacktype_t', 'i2'), # NIH_STACKTYPE_TYPE +] + +NIH_COLORTABLE_TYPE = ( + 'CustomTable', 'AppleDefault', 'Pseudo20', 'Pseudo32', 'Rainbow', + 'Fire1', 'Fire2', 'Ice', 'Grays', 'Spectrum') + +NIH_LUTMODE_TYPE = ( + 'PseudoColor', 'OldAppleDefault', 'OldSpectrum', 'GrayScale', + 'ColorLut', 'CustomGrayscale') + +NIH_CURVEFIT_TYPE = ( + 'StraightLine', 'Poly2', 'Poly3', 'Poly4', 'Poly5', 'ExpoFit', + 'PowerFit', 'LogFit', 'RodbardFit', 'SpareFit1', 'Uncalibrated', + 'UncalibratedOD') + +NIH_UNITS_TYPE = ( + 'Nanometers', 'Micrometers', 'Millimeters', 'Centimeters', 'Meters', + 'Kilometers', 'Inches', 'Feet', 'Miles', 'Pixels', 'OtherUnits') + +NIH_STACKTYPE_TYPE = ( + 'VolumeStack', 'RGBStack', 'MovieStack', 'HSVStack') + +# Map Universal Imaging Corporation MetaMorph internal tag ids to name and type +UIC_TAGS = { + 0: ('auto_scale', int), + 1: ('min_scale', int), + 2: ('max_scale', int), + 3: ('spatial_calibration', int), + 4: ('x_calibration', Fraction), + 5: ('y_calibration', Fraction), + 6: ('calibration_units', str), + 7: ('name', str), + 8: ('thresh_state', int), + 9: ('thresh_state_red', int), + 10: ('tagid_10', None), # undefined + 11: ('thresh_state_green', int), + 12: ('thresh_state_blue', int), + 13: ('thresh_state_lo', int), + 14: ('thresh_state_hi', int), + 15: ('zoom', int), + 16: ('create_time', julian_datetime), + 17: ('last_saved_time', julian_datetime), + 18: ('current_buffer', int), + 19: ('gray_fit', None), + 20: ('gray_point_count', None), + 21: ('gray_x', Fraction), + 22: ('gray_y', Fraction), + 23: ('gray_min', Fraction), + 24: ('gray_max', Fraction), + 25: ('gray_unit_name', str), + 26: ('standard_lut', int), + 27: ('wavelength', int), + 28: ('stage_position', '(%i,2,2)u4'), # N xy positions as fractions + 29: ('camera_chip_offset', '(%i,2,2)u4'), # N xy offsets as fractions + 30: ('overlay_mask', None), + 31: ('overlay_compress', None), + 32: ('overlay', None), + 33: ('special_overlay_mask', None), + 34: ('special_overlay_compress', None), + 35: ('special_overlay', None), + 36: ('image_property', read_uic_image_property), + 37: ('stage_label', '%ip'), # N str + 38: ('autoscale_lo_info', Fraction), + 39: ('autoscale_hi_info', Fraction), + 40: ('absolute_z', '(%i,2)u4'), # N fractions + 41: ('absolute_z_valid', '(%i,)u4'), # N long + 42: ('gamma', int), + 43: ('gamma_red', int), + 44: ('gamma_green', int), + 45: ('gamma_blue', int), + 46: ('camera_bin', int), + 47: ('new_lut', int), + 48: ('image_property_ex', None), + 49: ('plane_property', int), + 50: ('user_lut_table', '(256,3)u1'), + 51: ('red_autoscale_info', int), + 52: ('red_autoscale_lo_info', Fraction), + 53: ('red_autoscale_hi_info', Fraction), + 54: ('red_minscale_info', int), + 55: ('red_maxscale_info', int), + 56: ('green_autoscale_info', int), + 57: ('green_autoscale_lo_info', Fraction), + 58: ('green_autoscale_hi_info', Fraction), + 59: ('green_minscale_info', int), + 60: ('green_maxscale_info', int), + 61: ('blue_autoscale_info', int), + 62: ('blue_autoscale_lo_info', Fraction), + 63: ('blue_autoscale_hi_info', Fraction), + 64: ('blue_min_scale_info', int), + 65: ('blue_max_scale_info', int), + #66: ('overlay_plane_color', read_uic_overlay_plane_color), +} + + +# Olympus FluoView +MM_DIMENSION = [ + ('name', 'a16'), + ('size', 'i4'), + ('origin', 'f8'), + ('resolution', 'f8'), + ('unit', 'a64'), +] + +MM_HEADER = [ + ('header_flag', 'i2'), + ('image_type', 'u1'), + ('image_name', 'a257'), + ('offset_data', 'u4'), + ('palette_size', 'i4'), + ('offset_palette0', 'u4'), + ('offset_palette1', 'u4'), + ('comment_size', 'i4'), + ('offset_comment', 'u4'), + ('dimensions', MM_DIMENSION, 10), + ('offset_position', 'u4'), + ('map_type', 'i2'), + ('map_min', 'f8'), + ('map_max', 'f8'), + ('min_value', 'f8'), + ('max_value', 'f8'), + ('offset_map', 'u4'), + ('gamma', 'f8'), + ('offset', 'f8'), + ('gray_channel', MM_DIMENSION), + ('offset_thumbnail', 'u4'), + ('voice_field', 'i4'), + ('offset_voice_field', 'u4'), +] + +# Carl Zeiss LSM +CZ_LSM_INFO = [ + ('magic_number', 'u4'), + ('structure_size', 'i4'), + ('dimension_x', 'i4'), + ('dimension_y', 'i4'), + ('dimension_z', 'i4'), + ('dimension_channels', 'i4'), + ('dimension_time', 'i4'), + ('data_type', 'i4'), # CZ_DATA_TYPES + ('thumbnail_x', 'i4'), + ('thumbnail_y', 'i4'), + ('voxel_size_x', 'f8'), + ('voxel_size_y', 'f8'), + ('voxel_size_z', 'f8'), + ('origin_x', 'f8'), + ('origin_y', 'f8'), + ('origin_z', 'f8'), + ('scan_type', 'u2'), + ('spectral_scan', 'u2'), + ('type_of_data', 'u4'), # CZ_TYPE_OF_DATA + ('offset_vector_overlay', 'u4'), + ('offset_input_lut', 'u4'), + ('offset_output_lut', 'u4'), + ('offset_channel_colors', 'u4'), + ('time_interval', 'f8'), + ('offset_channel_data_types', 'u4'), + ('offset_scan_info', 'u4'), # CZ_LSM_SCAN_INFO + ('offset_ks_data', 'u4'), + ('offset_time_stamps', 'u4'), + ('offset_event_list', 'u4'), + ('offset_roi', 'u4'), + ('offset_bleach_roi', 'u4'), + ('offset_next_recording', 'u4'), + # LSM 2.0 ends here + ('display_aspect_x', 'f8'), + ('display_aspect_y', 'f8'), + ('display_aspect_z', 'f8'), + ('display_aspect_time', 'f8'), + ('offset_mean_of_roi_overlay', 'u4'), + ('offset_topo_isoline_overlay', 'u4'), + ('offset_topo_profile_overlay', 'u4'), + ('offset_linescan_overlay', 'u4'), + ('offset_toolbar_flags', 'u4'), + ('offset_channel_wavelength', 'u4'), + ('offset_channel_factors', 'u4'), + ('objective_sphere_correction', 'f8'), + ('offset_unmix_parameters', 'u4'), + # LSM 3.2, 4.0 end here + ('offset_acquisition_parameters', 'u4'), + ('offset_characteristics', 'u4'), + ('offset_palette', 'u4'), + ('time_difference_x', 'f8'), + ('time_difference_y', 'f8'), + ('time_difference_z', 'f8'), + ('internal_use_1', 'u4'), + ('dimension_p', 'i4'), + ('dimension_m', 'i4'), + ('dimensions_reserved', '16i4'), + ('offset_tile_positions', 'u4'), + ('reserved_1', '9u4'), + ('offset_positions', 'u4'), + ('reserved_2', '21u4'), # must be 0 +] + +# Import functions for LSM_INFO sub-records +CZ_LSM_INFO_READERS = { + 'scan_info': read_cz_lsm_scan_info, + 'time_stamps': read_cz_lsm_time_stamps, + 'event_list': read_cz_lsm_event_list, + 'channel_colors': read_cz_lsm_floatpairs, + 'positions': read_cz_lsm_floatpairs, + 'tile_positions': read_cz_lsm_floatpairs, +} + +# Map cz_lsm_info.scan_type to dimension order +CZ_SCAN_TYPES = { + 0: 'XYZCT', # x-y-z scan + 1: 'XYZCT', # z scan (x-z plane) + 2: 'XYZCT', # line scan + 3: 'XYTCZ', # time series x-y + 4: 'XYZTC', # time series x-z + 5: 'XYTCZ', # time series 'Mean of ROIs' + 6: 'XYZTC', # time series x-y-z + 7: 'XYCTZ', # spline scan + 8: 'XYCZT', # spline scan x-z + 9: 'XYTCZ', # time series spline plane x-z + 10: 'XYZCT', # point mode +} + +# Map dimension codes to cz_lsm_info attribute +CZ_DIMENSIONS = { + 'X': 'dimension_x', + 'Y': 'dimension_y', + 'Z': 'dimension_z', + 'C': 'dimension_channels', + 'T': 'dimension_time', +} + +# Description of cz_lsm_info.data_type +CZ_DATA_TYPES = { + 0: 'varying data types', + 1: '8 bit unsigned integer', + 2: '12 bit unsigned integer', + 5: '32 bit float', +} + +# Description of cz_lsm_info.type_of_data +CZ_TYPE_OF_DATA = { + 0: 'Original scan data', + 1: 'Calculated data', + 2: '3D reconstruction', + 3: 'Topography height map', +} + +CZ_LSM_SCAN_INFO_ARRAYS = { + 0x20000000: "tracks", + 0x30000000: "lasers", + 0x60000000: "detection_channels", + 0x80000000: "illumination_channels", + 0xa0000000: "beam_splitters", + 0xc0000000: "data_channels", + 0x11000000: "timers", + 0x13000000: "markers", +} + +CZ_LSM_SCAN_INFO_STRUCTS = { + # 0x10000000: "recording", + 0x40000000: "track", + 0x50000000: "laser", + 0x70000000: "detection_channel", + 0x90000000: "illumination_channel", + 0xb0000000: "beam_splitter", + 0xd0000000: "data_channel", + 0x12000000: "timer", + 0x14000000: "marker", +} + +CZ_LSM_SCAN_INFO_ATTRIBUTES = { + # recording + 0x10000001: "name", + 0x10000002: "description", + 0x10000003: "notes", + 0x10000004: "objective", + 0x10000005: "processing_summary", + 0x10000006: "special_scan_mode", + 0x10000007: "scan_type", + 0x10000008: "scan_mode", + 0x10000009: "number_of_stacks", + 0x1000000a: "lines_per_plane", + 0x1000000b: "samples_per_line", + 0x1000000c: "planes_per_volume", + 0x1000000d: "images_width", + 0x1000000e: "images_height", + 0x1000000f: "images_number_planes", + 0x10000010: "images_number_stacks", + 0x10000011: "images_number_channels", + 0x10000012: "linscan_xy_size", + 0x10000013: "scan_direction", + 0x10000014: "time_series", + 0x10000015: "original_scan_data", + 0x10000016: "zoom_x", + 0x10000017: "zoom_y", + 0x10000018: "zoom_z", + 0x10000019: "sample_0x", + 0x1000001a: "sample_0y", + 0x1000001b: "sample_0z", + 0x1000001c: "sample_spacing", + 0x1000001d: "line_spacing", + 0x1000001e: "plane_spacing", + 0x1000001f: "plane_width", + 0x10000020: "plane_height", + 0x10000021: "volume_depth", + 0x10000023: "nutation", + 0x10000034: "rotation", + 0x10000035: "precession", + 0x10000036: "sample_0time", + 0x10000037: "start_scan_trigger_in", + 0x10000038: "start_scan_trigger_out", + 0x10000039: "start_scan_event", + 0x10000040: "start_scan_time", + 0x10000041: "stop_scan_trigger_in", + 0x10000042: "stop_scan_trigger_out", + 0x10000043: "stop_scan_event", + 0x10000044: "stop_scan_time", + 0x10000045: "use_rois", + 0x10000046: "use_reduced_memory_rois", + 0x10000047: "user", + 0x10000048: "use_bc_correction", + 0x10000049: "position_bc_correction1", + 0x10000050: "position_bc_correction2", + 0x10000051: "interpolation_y", + 0x10000052: "camera_binning", + 0x10000053: "camera_supersampling", + 0x10000054: "camera_frame_width", + 0x10000055: "camera_frame_height", + 0x10000056: "camera_offset_x", + 0x10000057: "camera_offset_y", + 0x10000059: "rt_binning", + 0x1000005a: "rt_frame_width", + 0x1000005b: "rt_frame_height", + 0x1000005c: "rt_region_width", + 0x1000005d: "rt_region_height", + 0x1000005e: "rt_offset_x", + 0x1000005f: "rt_offset_y", + 0x10000060: "rt_zoom", + 0x10000061: "rt_line_period", + 0x10000062: "prescan", + 0x10000063: "scan_direction_z", + # track + 0x40000001: "multiplex_type", # 0 after line; 1 after frame + 0x40000002: "multiplex_order", + 0x40000003: "sampling_mode", # 0 sample; 1 line average; 2 frame average + 0x40000004: "sampling_method", # 1 mean; 2 sum + 0x40000005: "sampling_number", + 0x40000006: "acquire", + 0x40000007: "sample_observation_time", + 0x4000000b: "time_between_stacks", + 0x4000000c: "name", + 0x4000000d: "collimator1_name", + 0x4000000e: "collimator1_position", + 0x4000000f: "collimator2_name", + 0x40000010: "collimator2_position", + 0x40000011: "is_bleach_track", + 0x40000012: "is_bleach_after_scan_number", + 0x40000013: "bleach_scan_number", + 0x40000014: "trigger_in", + 0x40000015: "trigger_out", + 0x40000016: "is_ratio_track", + 0x40000017: "bleach_count", + 0x40000018: "spi_center_wavelength", + 0x40000019: "pixel_time", + 0x40000021: "condensor_frontlens", + 0x40000023: "field_stop_value", + 0x40000024: "id_condensor_aperture", + 0x40000025: "condensor_aperture", + 0x40000026: "id_condensor_revolver", + 0x40000027: "condensor_filter", + 0x40000028: "id_transmission_filter1", + 0x40000029: "id_transmission1", + 0x40000030: "id_transmission_filter2", + 0x40000031: "id_transmission2", + 0x40000032: "repeat_bleach", + 0x40000033: "enable_spot_bleach_pos", + 0x40000034: "spot_bleach_posx", + 0x40000035: "spot_bleach_posy", + 0x40000036: "spot_bleach_posz", + 0x40000037: "id_tubelens", + 0x40000038: "id_tubelens_position", + 0x40000039: "transmitted_light", + 0x4000003a: "reflected_light", + 0x4000003b: "simultan_grab_and_bleach", + 0x4000003c: "bleach_pixel_time", + # laser + 0x50000001: "name", + 0x50000002: "acquire", + 0x50000003: "power", + # detection_channel + 0x70000001: "integration_mode", + 0x70000002: "special_mode", + 0x70000003: "detector_gain_first", + 0x70000004: "detector_gain_last", + 0x70000005: "amplifier_gain_first", + 0x70000006: "amplifier_gain_last", + 0x70000007: "amplifier_offs_first", + 0x70000008: "amplifier_offs_last", + 0x70000009: "pinhole_diameter", + 0x7000000a: "counting_trigger", + 0x7000000b: "acquire", + 0x7000000c: "point_detector_name", + 0x7000000d: "amplifier_name", + 0x7000000e: "pinhole_name", + 0x7000000f: "filter_set_name", + 0x70000010: "filter_name", + 0x70000013: "integrator_name", + 0x70000014: "channel_name", + 0x70000015: "detector_gain_bc1", + 0x70000016: "detector_gain_bc2", + 0x70000017: "amplifier_gain_bc1", + 0x70000018: "amplifier_gain_bc2", + 0x70000019: "amplifier_offset_bc1", + 0x70000020: "amplifier_offset_bc2", + 0x70000021: "spectral_scan_channels", + 0x70000022: "spi_wavelength_start", + 0x70000023: "spi_wavelength_stop", + 0x70000026: "dye_name", + 0x70000027: "dye_folder", + # illumination_channel + 0x90000001: "name", + 0x90000002: "power", + 0x90000003: "wavelength", + 0x90000004: "aquire", + 0x90000005: "detchannel_name", + 0x90000006: "power_bc1", + 0x90000007: "power_bc2", + # beam_splitter + 0xb0000001: "filter_set", + 0xb0000002: "filter", + 0xb0000003: "name", + # data_channel + 0xd0000001: "name", + 0xd0000003: "acquire", + 0xd0000004: "color", + 0xd0000005: "sample_type", + 0xd0000006: "bits_per_sample", + 0xd0000007: "ratio_type", + 0xd0000008: "ratio_track1", + 0xd0000009: "ratio_track2", + 0xd000000a: "ratio_channel1", + 0xd000000b: "ratio_channel2", + 0xd000000c: "ratio_const1", + 0xd000000d: "ratio_const2", + 0xd000000e: "ratio_const3", + 0xd000000f: "ratio_const4", + 0xd0000010: "ratio_const5", + 0xd0000011: "ratio_const6", + 0xd0000012: "ratio_first_images1", + 0xd0000013: "ratio_first_images2", + 0xd0000014: "dye_name", + 0xd0000015: "dye_folder", + 0xd0000016: "spectrum", + 0xd0000017: "acquire", + # timer + 0x12000001: "name", + 0x12000002: "description", + 0x12000003: "interval", + 0x12000004: "trigger_in", + 0x12000005: "trigger_out", + 0x12000006: "activation_time", + 0x12000007: "activation_number", + # marker + 0x14000001: "name", + 0x14000002: "description", + 0x14000003: "trigger_in", + 0x14000004: "trigger_out", +} + +# Map TIFF tag code to attribute name, default value, type, count, validator +TIFF_TAGS = { + 254: ('new_subfile_type', 0, 4, 1, TIFF_SUBFILE_TYPES()), + 255: ('subfile_type', None, 3, 1, + {0: 'undefined', 1: 'image', 2: 'reduced_image', 3: 'page'}), + 256: ('image_width', None, 4, 1, None), + 257: ('image_length', None, 4, 1, None), + 258: ('bits_per_sample', 1, 3, 1, None), + 259: ('compression', 1, 3, 1, TIFF_COMPESSIONS), + 262: ('photometric', None, 3, 1, TIFF_PHOTOMETRICS), + 266: ('fill_order', 1, 3, 1, {1: 'msb2lsb', 2: 'lsb2msb'}), + 269: ('document_name', None, 2, None, None), + 270: ('image_description', None, 2, None, None), + 271: ('make', None, 2, None, None), + 272: ('model', None, 2, None, None), + 273: ('strip_offsets', None, 4, None, None), + 274: ('orientation', 1, 3, 1, TIFF_ORIENTATIONS), + 277: ('samples_per_pixel', 1, 3, 1, None), + 278: ('rows_per_strip', 2**32-1, 4, 1, None), + 279: ('strip_byte_counts', None, 4, None, None), + 280: ('min_sample_value', None, 3, None, None), + 281: ('max_sample_value', None, 3, None, None), # 2**bits_per_sample + 282: ('x_resolution', None, 5, 1, None), + 283: ('y_resolution', None, 5, 1, None), + 284: ('planar_configuration', 1, 3, 1, {1: 'contig', 2: 'separate'}), + 285: ('page_name', None, 2, None, None), + 286: ('x_position', None, 5, 1, None), + 287: ('y_position', None, 5, 1, None), + 296: ('resolution_unit', 2, 4, 1, {1: 'none', 2: 'inch', 3: 'centimeter'}), + 297: ('page_number', None, 3, 2, None), + 305: ('software', None, 2, None, None), + 306: ('datetime', None, 2, None, None), + 315: ('artist', None, 2, None, None), + 316: ('host_computer', None, 2, None, None), + 317: ('predictor', 1, 3, 1, {1: None, 2: 'horizontal'}), + 318: ('white_point', None, 5, 2, None), + 319: ('primary_chromaticities', None, 5, 6, None), + 320: ('color_map', None, 3, None, None), + 322: ('tile_width', None, 4, 1, None), + 323: ('tile_length', None, 4, 1, None), + 324: ('tile_offsets', None, 4, None, None), + 325: ('tile_byte_counts', None, 4, None, None), + 338: ('extra_samples', None, 3, None, + {0: 'unspecified', 1: 'assocalpha', 2: 'unassalpha'}), + 339: ('sample_format', 1, 3, 1, TIFF_SAMPLE_FORMATS), + 340: ('smin_sample_value', None, None, None, None), + 341: ('smax_sample_value', None, None, None, None), + 347: ('jpeg_tables', None, 7, None, None), + 530: ('ycbcr_subsampling', 1, 3, 2, None), + 531: ('ycbcr_positioning', 1, 3, 1, None), + 32996: ('sgi_matteing', None, None, 1, None), # use extra_samples + 32996: ('sgi_datatype', None, None, 1, None), # use sample_format + 32997: ('image_depth', None, 4, 1, None), + 32998: ('tile_depth', None, 4, 1, None), + 33432: ('copyright', None, 1, None, None), + 33445: ('md_file_tag', None, 4, 1, None), + 33446: ('md_scale_pixel', None, 5, 1, None), + 33447: ('md_color_table', None, 3, None, None), + 33448: ('md_lab_name', None, 2, None, None), + 33449: ('md_sample_info', None, 2, None, None), + 33450: ('md_prep_date', None, 2, None, None), + 33451: ('md_prep_time', None, 2, None, None), + 33452: ('md_file_units', None, 2, None, None), + 33550: ('model_pixel_scale', None, 12, 3, None), + 33922: ('model_tie_point', None, 12, None, None), + 34665: ('exif_ifd', None, None, 1, None), + 34735: ('geo_key_directory', None, 3, None, None), + 34736: ('geo_double_params', None, 12, None, None), + 34737: ('geo_ascii_params', None, 2, None, None), + 34853: ('gps_ifd', None, None, 1, None), + 37510: ('user_comment', None, None, None, None), + 42112: ('gdal_metadata', None, 2, None, None), + 42113: ('gdal_nodata', None, 2, None, None), + 50289: ('mc_xy_position', None, 12, 2, None), + 50290: ('mc_z_position', None, 12, 1, None), + 50291: ('mc_xy_calibration', None, 12, 3, None), + 50292: ('mc_lens_lem_na_n', None, 12, 3, None), + 50293: ('mc_channel_name', None, 1, None, None), + 50294: ('mc_ex_wavelength', None, 12, 1, None), + 50295: ('mc_time_stamp', None, 12, 1, None), + 50838: ('imagej_byte_counts', None, None, None, None), + 65200: ('flex_xml', None, 2, None, None), + # code: (attribute name, default value, type, count, validator) +} + +# Map custom TIFF tag codes to attribute names and import functions +CUSTOM_TAGS = { + 700: ('xmp', read_bytes), + 34377: ('photoshop', read_numpy), + 33723: ('iptc', read_bytes), + 34675: ('icc_profile', read_bytes), + 33628: ('uic1tag', read_uic1tag), # Universal Imaging Corporation STK + 33629: ('uic2tag', read_uic2tag), + 33630: ('uic3tag', read_uic3tag), + 33631: ('uic4tag', read_uic4tag), + 34361: ('mm_header', read_mm_header), # Olympus FluoView + 34362: ('mm_stamp', read_mm_stamp), + 34386: ('mm_user_block', read_bytes), + 34412: ('cz_lsm_info', read_cz_lsm_info), # Carl Zeiss LSM + 43314: ('nih_image_header', read_nih_image_header), + # 40001: ('mc_ipwinscal', read_bytes), + 40100: ('mc_id_old', read_bytes), + 50288: ('mc_id', read_bytes), + 50296: ('mc_frame_properties', read_bytes), + 50839: ('imagej_metadata', read_bytes), + 51123: ('micromanager_metadata', read_json), +} + +# Max line length of printed output +PRINT_LINE_LEN = 79 + + +def imshow(data, title=None, vmin=0, vmax=None, cmap=None, + bitspersample=None, photometric='rgb', interpolation='nearest', + dpi=96, figure=None, subplot=111, maxdim=8192, **kwargs): + """Plot n-dimensional images using matplotlib.pyplot. + + Return figure, subplot and plot axis. + Requires pyplot already imported ``from matplotlib import pyplot``. + + Parameters + ---------- + bitspersample : int or None + Number of bits per channel in integer RGB images. + photometric : {'miniswhite', 'minisblack', 'rgb', or 'palette'} + The color space of the image data. + title : str + Window and subplot title. + figure : matplotlib.figure.Figure (optional). + Matplotlib to use for plotting. + subplot : int + A matplotlib.pyplot.subplot axis. + maxdim : int + maximum image size in any dimension. + kwargs : optional + Arguments for matplotlib.pyplot.imshow. + + """ + #if photometric not in ('miniswhite', 'minisblack', 'rgb', 'palette'): + # raise ValueError("Can't handle %s photometrics" % photometric) + # TODO: handle photometric == 'separated' (CMYK) + isrgb = photometric in ('rgb', 'palette') + data = numpy.atleast_2d(data.squeeze()) + data = data[(slice(0, maxdim), ) * len(data.shape)] + + dims = data.ndim + if dims < 2: + raise ValueError("not an image") + elif dims == 2: + dims = 0 + isrgb = False + else: + if isrgb and data.shape[-3] in (3, 4): + data = numpy.swapaxes(data, -3, -2) + data = numpy.swapaxes(data, -2, -1) + elif not isrgb and (data.shape[-1] < data.shape[-2] // 16 and + data.shape[-1] < data.shape[-3] // 16 and + data.shape[-1] < 5): + data = numpy.swapaxes(data, -3, -1) + data = numpy.swapaxes(data, -2, -1) + isrgb = isrgb and data.shape[-1] in (3, 4) + dims -= 3 if isrgb else 2 + + if photometric == 'palette' and isrgb: + datamax = data.max() + if datamax > 255: + data >>= 8 # possible precision loss + data = data.astype('B') + elif data.dtype.kind in 'ui': + if not (isrgb and data.dtype.itemsize <= 1) or bitspersample is None: + try: + bitspersample = int(math.ceil(math.log(data.max(), 2))) + except Exception: + bitspersample = data.dtype.itemsize * 8 + elif not isinstance(bitspersample, int): + # bitspersample can be tuple, e.g. (5, 6, 5) + bitspersample = data.dtype.itemsize * 8 + datamax = 2**bitspersample + if isrgb: + if bitspersample < 8: + data <<= 8 - bitspersample + elif bitspersample > 8: + data >>= bitspersample - 8 # precision loss + data = data.astype('B') + elif data.dtype.kind == 'f': + datamax = data.max() + if isrgb and datamax > 1.0: + if data.dtype.char == 'd': + data = data.astype('f') + data /= datamax + elif data.dtype.kind == 'b': + datamax = 1 + elif data.dtype.kind == 'c': + raise NotImplementedError("complex type") # TODO: handle complex types + + if not isrgb: + if vmax is None: + vmax = datamax + if vmin is None: + if data.dtype.kind == 'i': + dtmin = numpy.iinfo(data.dtype).min + vmin = numpy.min(data) + if vmin == dtmin: + vmin = numpy.min(data > dtmin) + if data.dtype.kind == 'f': + dtmin = numpy.finfo(data.dtype).min + vmin = numpy.min(data) + if vmin == dtmin: + vmin = numpy.min(data > dtmin) + else: + vmin = 0 + + pyplot = sys.modules['matplotlib.pyplot'] + + if figure is None: + pyplot.rc('font', family='sans-serif', weight='normal', size=8) + figure = pyplot.figure(dpi=dpi, figsize=(10.3, 6.3), frameon=True, + facecolor='1.0', edgecolor='w') + try: + figure.canvas.manager.window.title(title) + except Exception: + pass + pyplot.subplots_adjust(bottom=0.03*(dims+2), top=0.9, + left=0.1, right=0.95, hspace=0.05, wspace=0.0) + subplot = pyplot.subplot(subplot) + + if title: + try: + title = unicode(title, 'Windows-1252') + except TypeError: + pass + pyplot.title(title, size=11) + + if cmap is None: + if data.dtype.kind in 'ubf' or vmin == 0: + cmap = 'cubehelix' + else: + cmap = 'coolwarm' + if photometric == 'miniswhite': + cmap += '_r' + + image = pyplot.imshow(data[(0, ) * dims].squeeze(), vmin=vmin, vmax=vmax, + cmap=cmap, interpolation=interpolation, **kwargs) + + if not isrgb: + pyplot.colorbar() # panchor=(0.55, 0.5), fraction=0.05 + + def format_coord(x, y): + # callback function to format coordinate display in toolbar + x = int(x + 0.5) + y = int(y + 0.5) + try: + if dims: + return "%s @ %s [%4i, %4i]" % (cur_ax_dat[1][y, x], + current, x, y) + else: + return "%s @ [%4i, %4i]" % (data[y, x], x, y) + except IndexError: + return "" + + pyplot.gca().format_coord = format_coord + + if dims: + current = list((0, ) * dims) + cur_ax_dat = [0, data[tuple(current)].squeeze()] + sliders = [pyplot.Slider( + pyplot.axes([0.125, 0.03*(axis+1), 0.725, 0.025]), + 'Dimension %i' % axis, 0, data.shape[axis]-1, 0, facecolor='0.5', + valfmt='%%.0f [%i]' % data.shape[axis]) for axis in range(dims)] + for slider in sliders: + slider.drawon = False + + def set_image(current, sliders=sliders, data=data): + # change image and redraw canvas + cur_ax_dat[1] = data[tuple(current)].squeeze() + image.set_data(cur_ax_dat[1]) + for ctrl, index in zip(sliders, current): + ctrl.eventson = False + ctrl.set_val(index) + ctrl.eventson = True + figure.canvas.draw() + + def on_changed(index, axis, data=data, current=current): + # callback function for slider change event + index = int(round(index)) + cur_ax_dat[0] = axis + if index == current[axis]: + return + if index >= data.shape[axis]: + index = 0 + elif index < 0: + index = data.shape[axis] - 1 + current[axis] = index + set_image(current) + + def on_keypressed(event, data=data, current=current): + # callback function for key press event + key = event.key + axis = cur_ax_dat[0] + if str(key) in '0123456789': + on_changed(key, axis) + elif key == 'right': + on_changed(current[axis] + 1, axis) + elif key == 'left': + on_changed(current[axis] - 1, axis) + elif key == 'up': + cur_ax_dat[0] = 0 if axis == len(data.shape)-1 else axis + 1 + elif key == 'down': + cur_ax_dat[0] = len(data.shape)-1 if axis == 0 else axis - 1 + elif key == 'end': + on_changed(data.shape[axis] - 1, axis) + elif key == 'home': + on_changed(0, axis) + + figure.canvas.mpl_connect('key_press_event', on_keypressed) + for axis, ctrl in enumerate(sliders): + ctrl.on_changed(lambda k, a=axis: on_changed(k, a)) + + return figure, subplot, image + + +def _app_show(): + """Block the GUI. For use as skimage plugin.""" + pyplot = sys.modules['matplotlib.pyplot'] + pyplot.show() + + +def main(argv=None): + """Command line usage main function.""" + if float(sys.version[0:3]) < 2.6: + print("This script requires Python version 2.6 or better.") + print("This is Python version %s" % sys.version) + return 0 + if argv is None: + argv = sys.argv + + import optparse + + parser = optparse.OptionParser( + usage="usage: %prog [options] path", + description="Display image data in TIFF files.", + version="%%prog %s" % __version__) + opt = parser.add_option + opt('-p', '--page', dest='page', type='int', default=-1, + help="display single page") + opt('-s', '--series', dest='series', type='int', default=-1, + help="display series of pages of same shape") + opt('--nomultifile', dest='nomultifile', action='store_true', + default=False, help="don't read OME series from multiple files") + opt('--noplot', dest='noplot', action='store_true', default=False, + help="don't display images") + opt('--interpol', dest='interpol', metavar='INTERPOL', default='bilinear', + help="image interpolation method") + opt('--dpi', dest='dpi', type='int', default=96, + help="set plot resolution") + opt('--debug', dest='debug', action='store_true', default=False, + help="raise exception on failures") + opt('--test', dest='test', action='store_true', default=False, + help="try read all images in path") + opt('--doctest', dest='doctest', action='store_true', default=False, + help="runs the docstring examples") + opt('-v', '--verbose', dest='verbose', action='store_true', default=True) + opt('-q', '--quiet', dest='verbose', action='store_false') + + settings, path = parser.parse_args() + path = ' '.join(path) + + if settings.doctest: + import doctest + doctest.testmod() + return 0 + if not path: + parser.error("No file specified") + if settings.test: + test_tifffile(path, settings.verbose) + return 0 + + if any(i in path for i in '?*'): + path = glob.glob(path) + if not path: + print('no files match the pattern') + return 0 + # TODO: handle image sequences + #if len(path) == 1: + path = path[0] + + print("Reading file structure...", end=' ') + start = time.time() + try: + tif = TiffFile(path, multifile=not settings.nomultifile) + except Exception as e: + if settings.debug: + raise + else: + print("\n", e) + sys.exit(0) + print("%.3f ms" % ((time.time()-start) * 1e3)) + + if tif.is_ome: + settings.norgb = True + + images = [(None, tif[0 if settings.page < 0 else settings.page])] + if not settings.noplot: + print("Reading image data... ", end=' ') + + def notnone(x): + return next(i for i in x if i is not None) + start = time.time() + try: + if settings.page >= 0: + images = [(tif.asarray(key=settings.page), + tif[settings.page])] + elif settings.series >= 0: + images = [(tif.asarray(series=settings.series), + notnone(tif.series[settings.series].pages))] + else: + images = [] + for i, s in enumerate(tif.series): + try: + images.append( + (tif.asarray(series=i), notnone(s.pages))) + except ValueError as e: + images.append((None, notnone(s.pages))) + if settings.debug: + raise + else: + print("\n* series %i failed: %s... " % (i, e), + end='') + print("%.3f ms" % ((time.time()-start) * 1e3)) + except Exception as e: + if settings.debug: + raise + else: + print(e) + + tif.close() + + print("\nTIFF file:", tif) + print() + for i, s in enumerate(tif.series): + print ("Series %i" % i) + print(s) + print() + for i, page in images: + print(page) + print(page.tags) + if page.is_palette: + print("\nColor Map:", page.color_map.shape, page.color_map.dtype) + for attr in ('cz_lsm_info', 'cz_lsm_scan_info', 'uic_tags', + 'mm_header', 'imagej_tags', 'micromanager_metadata', + 'nih_image_header'): + if hasattr(page, attr): + print("", attr.upper(), Record(getattr(page, attr)), sep="\n") + print() + if page.is_micromanager: + print('MICROMANAGER_FILE_METADATA') + print(Record(tif.micromanager_metadata)) + + if images and not settings.noplot: + try: + import matplotlib + matplotlib.use('TkAgg') + from matplotlib import pyplot + except ImportError as e: + warnings.warn("failed to import matplotlib.\n%s" % e) + else: + for img, page in images: + if img is None: + continue + vmin, vmax = None, None + if 'gdal_nodata' in page.tags: + try: + vmin = numpy.min(img[img > float(page.gdal_nodata)]) + except ValueError: + pass + if page.is_stk: + try: + vmin = page.uic_tags['min_scale'] + vmax = page.uic_tags['max_scale'] + except KeyError: + pass + else: + if vmax <= vmin: + vmin, vmax = None, None + title = "%s\n %s" % (str(tif), str(page)) + imshow(img, title=title, vmin=vmin, vmax=vmax, + bitspersample=page.bits_per_sample, + photometric=page.photometric, + interpolation=settings.interpol, + dpi=settings.dpi) + pyplot.show() + + +TIFFfile = TiffFile # backwards compatibility + +if sys.version_info[0] > 2: + basestring = str, bytes + unicode = str + +if __name__ == "__main__": + sys.exit(main()) From 8049c642afa2e2ba79682f6b68e005834c090011 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 5 Oct 2014 13:06:49 -0500 Subject: [PATCH 0501/1122] Add tests for new behavior. Add tests for new behavior Fix utils import and make io.tests a package --- skimage/io/_plugins/pil_plugin.py | 51 +++++++++++++------ skimage/io/tests/__init__.py | 0 skimage/io/tests/test_pil.py | 13 +++++ skimage/io/tests/utils.py | 82 +++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 15 deletions(-) create mode 100644 skimage/io/tests/__init__.py create mode 100644 skimage/io/tests/utils.py diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 77368178..2d548533 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -29,7 +29,12 @@ def imread(fname, dtype=None): """ - if fname.lower().endswith(('tif', 'tiff')) and dtype is None: + if hasattr(fname, 'name'): + name = fname.name.lower() + else: + name = fname.lower() + + if name.endswith(('.tiff', '.tif')) and dtype is None: return tif_imread(fname) im = Image.open(fname) @@ -104,18 +109,9 @@ def ndarray_to_pil(arr, format_str=None): Refer to ``imsave``. """ - arr = np.asarray(arr).squeeze() - - if arr.ndim not in (2, 3): - raise ValueError("Invalid shape for image array: %s" % arr.shape) - - if arr.ndim == 3: - if arr.shape[2] not in (3, 4): - raise ValueError("Invalid number of channels in image array.") - if arr.ndim == 3: arr = img_as_ubyte(arr) - mode_base = mode = {3: 'RGB', 4: 'RGBA'}[arr.shape[2]] + mode = {3: 'RGB', 4: 'RGBA'}[arr.shape[2]] elif arr.dtype.kind == 'f': arr = img_as_uint(arr) @@ -137,8 +133,16 @@ def ndarray_to_pil(arr, format_str=None): mode = 'I' mode_base = 'I' - im = Image.new(mode_base, arr.T.shape) - im.fromstring(arr.tostring(), 'raw', mode) + if arr.ndim == 2: + im = Image.new(mode_base, arr.T.shape) + im.fromstring(arr.tostring(), 'raw', mode) + else: + try: + im = Image.frombytes(mode, (arr.shape[1], arr.shape[0]), + arr.tostring()) + except AttributeError: + im = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), + arr.tostring()) return im @@ -169,8 +173,25 @@ def imsave(fname, arr, format_str=None): if not isinstance(fname, string_types) and format_str is None: format_str = "PNG" - if fname.lower().endswith(('.tiff', '.tif')): - tif_imsave(fname) + if arr.ndim not in (2, 3): + raise ValueError("Invalid shape for image array: %s" % arr.shape) + + if arr.ndim == 3: + if arr.shape[2] not in (3, 4): + raise ValueError("Invalid number of channels in image array.") + + arr = np.asanyarray(arr).squeeze() + + if arr.dtype.kind == 'b': + arr = arr.astype(np.uint8) + + if hasattr(fname, 'name'): + name = fname.name.lower() + else: + name = fname.lower() + + if name.endswith(('.tiff', '.tif')): + tif_imsave(fname, arr) else: img = ndarray_to_pil(arr, format_str=None) diff --git a/skimage/io/tests/__init__.py b/skimage/io/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index 5cd36f92..20ec5b8e 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -8,6 +8,7 @@ from tempfile import NamedTemporaryFile from skimage import data_dir from skimage.io import (imread, imsave, use_plugin, reset_plugins, Image as ioImage) +from skimage.io.test.utils import ubyte_check, full_range_check from six import BytesIO @@ -174,5 +175,17 @@ def test_imexport_imimport(): assert out.shape == shape +@skip(not PIL_available) +def test_all_color(self): + ubyte_check('pil') + ubyte_check('pil', 'bmp') + + +@skip(not PIL_available) +def test_all_mono(self): + full_range_check('pil') + full_range_check('pil', 'tiff') + + if __name__ == "__main__": run_module_suite() diff --git a/skimage/io/tests/utils.py b/skimage/io/tests/utils.py new file mode 100644 index 00000000..1c4482f3 --- /dev/null +++ b/skimage/io/tests/utils.py @@ -0,0 +1,82 @@ +from tempfile import NamedTemporaryFile + +from skimage import ( + data, io, img_as_uint, img_as_bool, img_as_float, img_as_int, img_as_ubyte) +from numpy import testing +import numpy as np + + +def roundtrip(img, plugin, suffix): + """Save and read an image using a specified plugin""" + if not '.' in suffix: + suffix = '.' + suffix + temp_file = NamedTemporaryFile(suffix=suffix, delete=False) + temp_file.close() + fname = temp_file.name + io.imsave(fname, img, plugin=plugin) + return io.imread(fname, plugin=plugin) + + +def ubyte_check(plugin, fmt='png'): + """Check roundtrip behavior for images that can only be saved as uint8 + + All major input types should be handled as ubytes and read + back correctly. + """ + img = img_as_ubyte(data.chelsea()) + r1 = roundtrip(img, plugin, fmt) + testing.assert_allclose(img, r1) + + img2 = img > 128 + r2 = roundtrip(img2, plugin, fmt) + testing.assert_allclose(img2, img_as_bool(r2)) + + img3 = img_as_float(img) + r3 = roundtrip(img3, plugin, fmt) + testing.assert_allclose(r3, img) + + img4 = img_as_int(img) + r4 = roundtrip(img4, plugin, fmt) + testing.assert_allclose(r4, img) + + img5 = img_as_uint(img) + r5 = roundtrip(img5, plugin, fmt) + testing.assert_allclose(r5, img) + + +def full_range_check(plugin, fmt='png'): + """Check the roundtrip behavior for images that support most types. + + All major input types should be handled, except bool is treated + as ubyte and float can treated as uint16 or float. + """ + + img = img_as_ubyte(data.moon()) + r1 = roundtrip(img, plugin, fmt) + testing.assert_allclose(img, r1) + + img2 = img > 128 + r2 = roundtrip(img2, plugin, fmt) + testing.assert_allclose(img2.astype(np.uint8), r2) + + img3 = img_as_float(img) + r3 = roundtrip(img3, plugin, fmt) + if r3.dtype.kind == 'f': + testing.assert_allclose(img3, r3) + else: + testing.assert_allclose(r3, img_as_uint(img)) + + img4 = img_as_int(img) + r4 = roundtrip(img4, plugin, fmt) + testing.assert_allclose(r4, img4) + + img5 = img_as_uint(img) + r5 = roundtrip(img5, plugin, fmt) + testing.assert_allclose(r5, img5) + + +if __name__ == '__main__': + ubyte_check('pil') + full_range_check('pil') + ubyte_check('pil', 'bmp') + full_range_check('pil', 'tiff') From c5b2834084519aa06353a85758f444064f1d6875 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 5 Oct 2014 20:53:01 -0500 Subject: [PATCH 0502/1122] Remove old tifffile plugin in favor of using it in PIL plugin. --- skimage/io/_plugins/tifffile.py | 4844 ----------------------- skimage/io/_plugins/tifffile_plugin.ini | 3 - skimage/io/_plugins/tifffile_plugin.py | 6 - skimage/io/tests/test_tifffile.py | 62 - 4 files changed, 4915 deletions(-) delete mode 100644 skimage/io/_plugins/tifffile.py delete mode 100644 skimage/io/_plugins/tifffile_plugin.ini delete mode 100644 skimage/io/_plugins/tifffile_plugin.py delete mode 100644 skimage/io/tests/test_tifffile.py diff --git a/skimage/io/_plugins/tifffile.py b/skimage/io/_plugins/tifffile.py deleted file mode 100644 index 8e094429..00000000 --- a/skimage/io/_plugins/tifffile.py +++ /dev/null @@ -1,4844 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# tifffile.py - -# Copyright (c) 2008-2014, Christoph Gohlke -# Copyright (c) 2008-2014, The Regents of the University of California -# Produced at the Laboratory for Fluorescence Dynamics -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of the copyright holders nor the names of any -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -"""Read and write image data from and to TIFF files. - -Image and metadata can be read from TIFF, BigTIFF, OME-TIFF, STK, LSM, NIH, -SGI, ImageJ, MicroManager, FluoView, SEQ and GEL files. -Only a subset of the TIFF specification is supported, mainly uncompressed -and losslessly compressed 2**(0 to 6) bit integer, 16, 32 and 64-bit float, -grayscale and RGB(A) images, which are commonly used in bio-scientific imaging. -Specifically, reading JPEG and CCITT compressed image data or EXIF, IPTC, GPS, -and XMP metadata is not implemented. -Only primary info records are read for STK, FluoView, MicroManager, and -NIH image formats. - -TIFF, the Tagged Image File Format, is under the control of Adobe Systems. -BigTIFF allows for files greater than 4 GB. STK, LSM, FluoView, SGI, SEQ, GEL, -and OME-TIFF, are custom extensions defined by Molecular Devices (Universal -Imaging Corporation), Carl Zeiss MicroImaging, Olympus, Silicon Graphics -International, Media Cybernetics, Molecular Dynamics, and the Open Microscopy -Environment consortium respectively. - -For command line usage run ``python tifffile.py --help`` - -:Author: - `Christoph Gohlke `_ - -:Organization: - Laboratory for Fluorescence Dynamics, University of California, Irvine - -:Version: 2014.08.24 - -Requirements ------------- -* `CPython 2.7 or 3.4 `_ -* `Numpy 1.8.2 `_ -* `Matplotlib 1.4 `_ (optional for plotting) -* `Tifffile.c 2013.11.05 `_ - (recommended for faster decoding of PackBits and LZW encoded strings) - -Notes ------ -The API is not stable yet and might change between revisions. - -Tested on little-endian platforms only. - -Other Python packages and modules for reading bio-scientific TIFF files: - -* `Imread `_ -* `PyLibTiff `_ -* `SimpleITK `_ -* `PyLSM `_ -* `PyMca.TiffIO.py `_ (same as fabio.TiffIO) -* `BioImageXD.Readers `_ -* `Cellcognition.io `_ -* `CellProfiler.bioformats - `_ - -Acknowledgements ----------------- -* Egor Zindy, University of Manchester, for cz_lsm_scan_info specifics. -* Wim Lewis for a bug fix and some read_cz_lsm functions. -* Hadrien Mary for help on reading MicroManager files. - -References ----------- -(1) TIFF 6.0 Specification and Supplements. Adobe Systems Incorporated. - http://partners.adobe.com/public/developer/tiff/ -(2) TIFF File Format FAQ. http://www.awaresystems.be/imaging/tiff/faq.html -(3) MetaMorph Stack (STK) Image File Format. - http://support.meta.moleculardevices.com/docs/t10243.pdf -(4) Image File Format Description LSM 5/7 Release 6.0 (ZEN 2010). - Carl Zeiss MicroImaging GmbH. BioSciences. May 10, 2011 -(5) File Format Description - LSM 5xx Release 2.0. - http://ibb.gsf.de/homepage/karsten.rodenacker/IDL/Lsmfile.doc -(6) The OME-TIFF format. - http://www.openmicroscopy.org/site/support/file-formats/ome-tiff -(7) UltraQuant(r) Version 6.0 for Windows Start-Up Guide. - http://www.ultralum.com/images%20ultralum/pdf/UQStart%20Up%20Guide.pdf -(8) Micro-Manager File Formats. - http://www.micro-manager.org/wiki/Micro-Manager_File_Formats -(9) Tags for TIFF and Related Specifications. Digital Preservation. - http://www.digitalpreservation.gov/formats/content/tiff_tags.shtml - -Examples --------- ->>> data = numpy.random.rand(5, 301, 219) ->>> imsave('temp.tif', data) - ->>> image = imread('temp.tif') ->>> numpy.testing.assert_array_equal(image, data) - ->>> with TiffFile('temp.tif') as tif: -... images = tif.asarray() -... for page in tif: -... for tag in page.tags.values(): -... t = tag.name, tag.value -... image = page.asarray() - -""" - -from __future__ import division, print_function - -import sys -import os -import re -import glob -import math -import zlib -import time -import json -import struct -import warnings -import tempfile -import datetime -import collections -from fractions import Fraction -from xml.etree import cElementTree as etree - -import numpy - -try: - import _tifffile -except ImportError: - warnings.warn( - "failed to import the optional _tifffile C extension module.\n" - "Loading of some compressed images will be slow.\n" - "Tifffile.c can be obtained at http://www.lfd.uci.edu/~gohlke/") - -__version__ = '2014.08.24' -__docformat__ = 'restructuredtext en' -__all__ = ('imsave', 'imread', 'imshow', 'TiffFile', 'TiffWriter', - 'TiffSequence') - - -def imsave(filename, data, **kwargs): - """Write image data to TIFF file. - - Refer to the TiffWriter class and member functions for documentation. - - Parameters - ---------- - filename : str - Name of file to write. - data : array_like - Input image. The last dimensions are assumed to be image depth, - height, width, and samples. - kwargs : dict - Parameters 'byteorder', 'bigtiff', and 'software' are passed to - the TiffWriter class. - Parameters 'photometric', 'planarconfig', 'resolution', - 'description', 'compress', 'volume', and 'extratags' are passed to - the TiffWriter.save function. - - Examples - -------- - >>> data = numpy.random.rand(2, 5, 3, 301, 219) - >>> description = u'{"shape": %s}' % str(list(data.shape)) - >>> imsave('temp.tif', data, compress=6, - ... extratags=[(270, 's', 0, description, True)]) - - """ - tifargs = {} - for key in ('byteorder', 'bigtiff', 'software', 'writeshape'): - if key in kwargs: - tifargs[key] = kwargs[key] - del kwargs[key] - - if 'writeshape' not in kwargs: - kwargs['writeshape'] = True - if 'bigtiff' not in tifargs and data.size*data.dtype.itemsize > 2000*2**20: - tifargs['bigtiff'] = True - - with TiffWriter(filename, **tifargs) as tif: - tif.save(data, **kwargs) - - -class TiffWriter(object): - """Write image data to TIFF file. - - TiffWriter instances must be closed using the close method, which is - automatically called when using the 'with' statement. - - Examples - -------- - >>> data = numpy.random.rand(2, 5, 3, 301, 219) - >>> with TiffWriter('temp.tif', bigtiff=True) as tif: - ... for i in range(data.shape[0]): - ... tif.save(data[i], compress=6) - - """ - TYPES = {'B': 1, 's': 2, 'H': 3, 'I': 4, '2I': 5, 'b': 6, - 'h': 8, 'i': 9, 'f': 11, 'd': 12, 'Q': 16, 'q': 17} - TAGS = { - 'new_subfile_type': 254, 'subfile_type': 255, - 'image_width': 256, 'image_length': 257, 'bits_per_sample': 258, - 'compression': 259, 'photometric': 262, 'fill_order': 266, - 'document_name': 269, 'image_description': 270, 'strip_offsets': 273, - 'orientation': 274, 'samples_per_pixel': 277, 'rows_per_strip': 278, - 'strip_byte_counts': 279, 'x_resolution': 282, 'y_resolution': 283, - 'planar_configuration': 284, 'page_name': 285, 'resolution_unit': 296, - 'software': 305, 'datetime': 306, 'predictor': 317, 'color_map': 320, - 'tile_width': 322, 'tile_length': 323, 'tile_offsets': 324, - 'tile_byte_counts': 325, 'extra_samples': 338, 'sample_format': 339, - 'image_depth': 32997, 'tile_depth': 32998} - - def __init__(self, filename, bigtiff=False, byteorder=None, - software='tifffile.py'): - """Create a new TIFF file for writing. - - Use bigtiff=True when creating files greater than 2 GB. - - Parameters - ---------- - filename : str - Name of file to write. - bigtiff : bool - If True, the BigTIFF format is used. - byteorder : {'<', '>'} - The endianness of the data in the file. - By default this is the system's native byte order. - software : str - Name of the software used to create the image. - Saved with the first page only. - - """ - if byteorder not in (None, '<', '>'): - raise ValueError("invalid byteorder %s" % byteorder) - if byteorder is None: - byteorder = '<' if sys.byteorder == 'little' else '>' - - self._byteorder = byteorder - self._software = software - - self._fh = open(filename, 'wb') - self._fh.write({'<': b'II', '>': b'MM'}[byteorder]) - - if bigtiff: - self._bigtiff = True - self._offset_size = 8 - self._tag_size = 20 - self._numtag_format = 'Q' - self._offset_format = 'Q' - self._val_format = '8s' - self._fh.write(struct.pack(byteorder+'HHH', 43, 8, 0)) - else: - self._bigtiff = False - self._offset_size = 4 - self._tag_size = 12 - self._numtag_format = 'H' - self._offset_format = 'I' - self._val_format = '4s' - self._fh.write(struct.pack(byteorder+'H', 42)) - - # first IFD - self._ifd_offset = self._fh.tell() - self._fh.write(struct.pack(byteorder+self._offset_format, 0)) - - def save(self, data, photometric=None, planarconfig=None, resolution=None, - description=None, volume=False, writeshape=False, compress=0, - extratags=()): - """Write image data to TIFF file. - - Image data are written in one stripe per plane. - Dimensions larger than 2 to 4 (depending on photometric mode, planar - configuration, and SGI mode) are flattened and saved as separate pages. - The 'sample_format' and 'bits_per_sample' TIFF tags are derived from - the data type. - - Parameters - ---------- - data : array_like - Input image. The last dimensions are assumed to be image depth, - height, width, and samples. - photometric : {'minisblack', 'miniswhite', 'rgb'} - The color space of the image data. - By default this setting is inferred from the data shape. - planarconfig : {'contig', 'planar'} - Specifies if samples are stored contiguous or in separate planes. - By default this setting is inferred from the data shape. - 'contig': last dimension contains samples. - 'planar': third last dimension contains samples. - resolution : (float, float) or ((int, int), (int, int)) - X and Y resolution in dots per inch as float or rational numbers. - description : str - The subject of the image. Saved with the first page only. - compress : int - Values from 0 to 9 controlling the level of zlib compression. - If 0, data are written uncompressed (default). - volume : bool - If True, volume data are stored in one tile (if applicable) using - the SGI image_depth and tile_depth tags. - Image width and depth must be multiple of 16. - Few software can read this format, e.g. MeVisLab. - writeshape : bool - If True, write the data shape to the image_description tag - if necessary and no other description is given. - extratags: sequence of tuples - Additional tags as [(code, dtype, count, value, writeonce)]. - - code : int - The TIFF tag Id. - dtype : str - Data type of items in 'value' in Python struct format. - One of B, s, H, I, 2I, b, h, i, f, d, Q, or q. - count : int - Number of data values. Not used for string values. - value : sequence - 'Count' values compatible with 'dtype'. - writeonce : bool - If True, the tag is written to the first page only. - - """ - if photometric not in (None, 'minisblack', 'miniswhite', 'rgb'): - raise ValueError("invalid photometric %s" % photometric) - if planarconfig not in (None, 'contig', 'planar'): - raise ValueError("invalid planarconfig %s" % planarconfig) - if not 0 <= compress <= 9: - raise ValueError("invalid compression level %s" % compress) - - fh = self._fh - byteorder = self._byteorder - numtag_format = self._numtag_format - val_format = self._val_format - offset_format = self._offset_format - offset_size = self._offset_size - tag_size = self._tag_size - - data = numpy.asarray(data, dtype=byteorder+data.dtype.char, order='C') - data_shape = shape = data.shape - data = numpy.atleast_2d(data) - - # normalize shape of data - samplesperpixel = 1 - extrasamples = 0 - if volume and data.ndim < 3: - volume = False - if photometric is None: - if planarconfig: - photometric = 'rgb' - elif data.ndim > 2 and shape[-1] in (3, 4): - photometric = 'rgb' - elif volume and data.ndim > 3 and shape[-4] in (3, 4): - photometric = 'rgb' - elif data.ndim > 2 and shape[-3] in (3, 4): - photometric = 'rgb' - else: - photometric = 'minisblack' - if planarconfig and len(shape) <= (3 if volume else 2): - planarconfig = None - photometric = 'minisblack' - if photometric == 'rgb': - if len(shape) < 3: - raise ValueError("not a RGB(A) image") - if len(shape) < 4: - volume = False - if planarconfig is None: - if shape[-1] in (3, 4): - planarconfig = 'contig' - elif shape[-4 if volume else -3] in (3, 4): - planarconfig = 'planar' - elif shape[-1] > shape[-4 if volume else -3]: - planarconfig = 'planar' - else: - planarconfig = 'contig' - if planarconfig == 'contig': - data = data.reshape((-1, 1) + shape[(-4 if volume else -3):]) - samplesperpixel = data.shape[-1] - else: - data = data.reshape( - (-1,) + shape[(-4 if volume else -3):] + (1,)) - samplesperpixel = data.shape[1] - if samplesperpixel > 3: - extrasamples = samplesperpixel - 3 - elif planarconfig and len(shape) > (3 if volume else 2): - if planarconfig == 'contig': - data = data.reshape((-1, 1) + shape[(-4 if volume else -3):]) - samplesperpixel = data.shape[-1] - else: - data = data.reshape( - (-1,) + shape[(-4 if volume else -3):] + (1,)) - samplesperpixel = data.shape[1] - extrasamples = samplesperpixel - 1 - else: - planarconfig = None - # remove trailing 1s - while len(shape) > 2 and shape[-1] == 1: - shape = shape[:-1] - if len(shape) < 3: - volume = False - if False and ( - len(shape) > (3 if volume else 2) and shape[-1] < 5 and - all(shape[-1] < i - for i in shape[(-4 if volume else -3):-1])): - # DISABLED: non-standard TIFF, e.g. (220, 320, 2) - planarconfig = 'contig' - samplesperpixel = shape[-1] - data = data.reshape((-1, 1) + shape[(-4 if volume else -3):]) - else: - data = data.reshape( - (-1, 1) + shape[(-3 if volume else -2):] + (1,)) - - if samplesperpixel == 2: - warnings.warn("writing non-standard TIFF (samplesperpixel 2)") - - if volume and (data.shape[-2] % 16 or data.shape[-3] % 16): - warnings.warn("volume width or length are not multiple of 16") - volume = False - data = numpy.swapaxes(data, 1, 2) - data = data.reshape( - (data.shape[0] * data.shape[1],) + data.shape[2:]) - - # data.shape is now normalized 5D or 6D, depending on volume - # (pages, planar_samples, (depth,) height, width, contig_samples) - assert len(data.shape) in (5, 6) - shape = data.shape - - bytestr = bytes if sys.version[0] == '2' else ( - lambda x: bytes(x, 'utf-8') if isinstance(x, str) else x) - tags = [] # list of (code, ifdentry, ifdvalue, writeonce) - - if volume: - # use tiles to save volume data - tag_byte_counts = TiffWriter.TAGS['tile_byte_counts'] - tag_offsets = TiffWriter.TAGS['tile_offsets'] - else: - # else use strips - tag_byte_counts = TiffWriter.TAGS['strip_byte_counts'] - tag_offsets = TiffWriter.TAGS['strip_offsets'] - - def pack(fmt, *val): - return struct.pack(byteorder+fmt, *val) - - def addtag(code, dtype, count, value, writeonce=False): - # Compute ifdentry & ifdvalue bytes from code, dtype, count, value. - # Append (code, ifdentry, ifdvalue, writeonce) to tags list. - code = int(TiffWriter.TAGS.get(code, code)) - try: - tifftype = TiffWriter.TYPES[dtype] - except KeyError: - raise ValueError("unknown dtype %s" % dtype) - rawcount = count - if dtype == 's': - value = bytestr(value) + b'\0' - count = rawcount = len(value) - value = (value, ) - if len(dtype) > 1: - count *= int(dtype[:-1]) - dtype = dtype[-1] - ifdentry = [pack('HH', code, tifftype), - pack(offset_format, rawcount)] - ifdvalue = None - if count == 1: - if isinstance(value, (tuple, list)): - value = value[0] - ifdentry.append(pack(val_format, pack(dtype, value))) - elif struct.calcsize(dtype) * count <= offset_size: - ifdentry.append(pack(val_format, - pack(str(count)+dtype, *value))) - else: - ifdentry.append(pack(offset_format, 0)) - ifdvalue = pack(str(count)+dtype, *value) - tags.append((code, b''.join(ifdentry), ifdvalue, writeonce)) - - def rational(arg, max_denominator=1000000): - # return nominator and denominator from float or two integers - try: - f = Fraction.from_float(arg) - except TypeError: - f = Fraction(arg[0], arg[1]) - f = f.limit_denominator(max_denominator) - return f.numerator, f.denominator - - if self._software: - addtag('software', 's', 0, self._software, writeonce=True) - self._software = None # only save to first page - if description: - addtag('image_description', 's', 0, description, writeonce=True) - elif writeshape and shape[0] > 1 and shape != data_shape: - addtag('image_description', 's', 0, - "shape=(%s)" % (",".join('%i' % i for i in data_shape)), - writeonce=True) - addtag('datetime', 's', 0, - datetime.datetime.now().strftime("%Y:%m:%d %H:%M:%S"), - writeonce=True) - addtag('compression', 'H', 1, 32946 if compress else 1) - addtag('orientation', 'H', 1, 1) - addtag('image_width', 'I', 1, shape[-2]) - addtag('image_length', 'I', 1, shape[-3]) - if volume: - addtag('image_depth', 'I', 1, shape[-4]) - addtag('tile_depth', 'I', 1, shape[-4]) - addtag('tile_width', 'I', 1, shape[-2]) - addtag('tile_length', 'I', 1, shape[-3]) - addtag('new_subfile_type', 'I', 1, 0 if shape[0] == 1 else 2) - addtag('sample_format', 'H', 1, - {'u': 1, 'i': 2, 'f': 3, 'c': 6}[data.dtype.kind]) - addtag('photometric', 'H', 1, - {'miniswhite': 0, 'minisblack': 1, 'rgb': 2}[photometric]) - addtag('samples_per_pixel', 'H', 1, samplesperpixel) - if planarconfig and samplesperpixel > 1: - addtag('planar_configuration', 'H', 1, 1 - if planarconfig == 'contig' else 2) - addtag('bits_per_sample', 'H', samplesperpixel, - (data.dtype.itemsize * 8, ) * samplesperpixel) - else: - addtag('bits_per_sample', 'H', 1, data.dtype.itemsize * 8) - if extrasamples: - if photometric == 'rgb' and extrasamples == 1: - addtag('extra_samples', 'H', 1, 1) # associated alpha channel - else: - addtag('extra_samples', 'H', extrasamples, (0,) * extrasamples) - if resolution: - addtag('x_resolution', '2I', 1, rational(resolution[0])) - addtag('y_resolution', '2I', 1, rational(resolution[1])) - addtag('resolution_unit', 'H', 1, 2) - addtag('rows_per_strip', 'I', 1, - shape[-3] * (shape[-4] if volume else 1)) - - # use one strip or tile per plane - strip_byte_counts = (data[0, 0].size * data.dtype.itemsize,) * shape[1] - addtag(tag_byte_counts, offset_format, shape[1], strip_byte_counts) - addtag(tag_offsets, offset_format, shape[1], (0, ) * shape[1]) - - # add extra tags from users - for t in extratags: - addtag(*t) - # the entries in an IFD must be sorted in ascending order by tag code - tags = sorted(tags, key=lambda x: x[0]) - - if not self._bigtiff and (fh.tell() + data.size*data.dtype.itemsize - > 2**31-1): - raise ValueError("data too large for non-bigtiff file") - - for pageindex in range(shape[0]): - # update pointer at ifd_offset - pos = fh.tell() - fh.seek(self._ifd_offset) - fh.write(pack(offset_format, pos)) - fh.seek(pos) - - # write ifdentries - fh.write(pack(numtag_format, len(tags))) - tag_offset = fh.tell() - fh.write(b''.join(t[1] for t in tags)) - self._ifd_offset = fh.tell() - fh.write(pack(offset_format, 0)) # offset to next IFD - - # write tag values and patch offsets in ifdentries, if necessary - for tagindex, tag in enumerate(tags): - if tag[2]: - pos = fh.tell() - fh.seek(tag_offset + tagindex*tag_size + offset_size + 4) - fh.write(pack(offset_format, pos)) - fh.seek(pos) - if tag[0] == tag_offsets: - strip_offsets_offset = pos - elif tag[0] == tag_byte_counts: - strip_byte_counts_offset = pos - fh.write(tag[2]) - - # write image data - data_offset = fh.tell() - if compress: - strip_byte_counts = [] - for plane in data[pageindex]: - plane = zlib.compress(plane, compress) - strip_byte_counts.append(len(plane)) - fh.write(plane) - else: - # if this fails try update Python/numpy - data[pageindex].tofile(fh) - fh.flush() - - # update strip and tile offsets and byte_counts if necessary - pos = fh.tell() - for tagindex, tag in enumerate(tags): - if tag[0] == tag_offsets: # strip or tile offsets - if tag[2]: - fh.seek(strip_offsets_offset) - strip_offset = data_offset - for size in strip_byte_counts: - fh.write(pack(offset_format, strip_offset)) - strip_offset += size - else: - fh.seek(tag_offset + tagindex*tag_size + - offset_size + 4) - fh.write(pack(offset_format, data_offset)) - elif tag[0] == tag_byte_counts: # strip or tile byte_counts - if compress: - if tag[2]: - fh.seek(strip_byte_counts_offset) - for size in strip_byte_counts: - fh.write(pack(offset_format, size)) - else: - fh.seek(tag_offset + tagindex*tag_size + - offset_size + 4) - fh.write(pack(offset_format, strip_byte_counts[0])) - break - fh.seek(pos) - fh.flush() - # remove tags that should be written only once - if pageindex == 0: - tags = [t for t in tags if not t[-1]] - - def close(self): - self._fh.close() - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - -def imread(files, **kwargs): - """Return image data from TIFF file(s) as numpy array. - - The first image series is returned if no arguments are provided. - - Parameters - ---------- - files : str or list - File name, glob pattern, or list of file names. - key : int, slice, or sequence of page indices - Defines which pages to return as array. - series : int - Defines which series of pages in file to return as array. - multifile : bool - If True (default), OME-TIFF data may include pages from multiple files. - pattern : str - Regular expression pattern that matches axes names and indices in - file names. - kwargs : dict - Additional parameters passed to the TiffFile or TiffSequence asarray - function. - - Examples - -------- - >>> im = imread('test.tif', key=0) - >>> im.shape - (256, 256, 4) - >>> ims = imread(['test.tif', 'test.tif']) - >>> ims.shape - (2, 256, 256, 4) - - """ - kwargs_file = {} - if 'multifile' in kwargs: - kwargs_file['multifile'] = kwargs['multifile'] - del kwargs['multifile'] - else: - kwargs_file['multifile'] = True - kwargs_seq = {} - if 'pattern' in kwargs: - kwargs_seq['pattern'] = kwargs['pattern'] - del kwargs['pattern'] - - if isinstance(files, basestring) and any(i in files for i in '?*'): - files = glob.glob(files) - if not files: - raise ValueError('no files found') - if len(files) == 1: - files = files[0] - - if isinstance(files, basestring): - with TiffFile(files, **kwargs_file) as tif: - return tif.asarray(**kwargs) - else: - with TiffSequence(files, **kwargs_seq) as imseq: - return imseq.asarray(**kwargs) - - -class lazyattr(object): - """Lazy object attribute whose value is computed on first access.""" - __slots__ = ('func', ) - - def __init__(self, func): - self.func = func - - def __get__(self, instance, owner): - if instance is None: - return self - value = self.func(instance) - if value is NotImplemented: - return getattr(super(owner, instance), self.func.__name__) - setattr(instance, self.func.__name__, value) - return value - - -class TiffFile(object): - """Read image and metadata from TIFF, STK, LSM, and FluoView files. - - TiffFile instances must be closed using the close method, which is - automatically called when using the 'with' statement. - - Attributes - ---------- - pages : list - All TIFF pages in file. - series : list of Records(shape, dtype, axes, TiffPages) - TIFF pages with compatible shapes and types. - micromanager_metadata: dict - Extra MicroManager non-TIFF metadata in the file, if exists. - - All attributes are read-only. - - Examples - -------- - >>> with TiffFile('test.tif') as tif: - ... data = tif.asarray() - ... data.shape - (256, 256, 4) - - """ - def __init__(self, arg, name=None, offset=None, size=None, - multifile=True, multifile_close=True): - """Initialize instance from file. - - Parameters - ---------- - arg : str or open file - Name of file or open file object. - The file objects are closed in TiffFile.close(). - name : str - Optional name of file in case 'arg' is a file handle. - offset : int - Optional start position of embedded file. By default this is - the current file position. - size : int - Optional size of embedded file. By default this is the number - of bytes from the 'offset' to the end of the file. - multifile : bool - If True (default), series may include pages from multiple files. - Currently applies to OME-TIFF only. - multifile_close : bool - If True (default), keep the handles of other files in multifile - series closed. This is inefficient when few files refer to - many pages. If False, the C runtime may run out of resources. - - """ - self._fh = FileHandle(arg, name=name, offset=offset, size=size) - self.offset_size = None - self.pages = [] - self._multifile = bool(multifile) - self._multifile_close = bool(multifile_close) - self._files = {self._fh.name: self} # cache of TiffFiles - try: - self._fromfile() - except Exception: - self._fh.close() - raise - - @property - def filehandle(self): - """Return file handle.""" - return self._fh - - @property - def filename(self): - """Return name of file handle.""" - return self._fh.name - - def close(self): - """Close open file handle(s).""" - for tif in self._files.values(): - tif._fh.close() - self._files = {} - - def _fromfile(self): - """Read TIFF header and all page records from file.""" - self._fh.seek(0) - try: - self.byteorder = {b'II': '<', b'MM': '>'}[self._fh.read(2)] - except KeyError: - raise ValueError("not a valid TIFF file") - version = struct.unpack(self.byteorder+'H', self._fh.read(2))[0] - if version == 43: # BigTiff - self.offset_size, zero = struct.unpack(self.byteorder+'HH', - self._fh.read(4)) - if zero or self.offset_size != 8: - raise ValueError("not a valid BigTIFF file") - elif version == 42: - self.offset_size = 4 - else: - raise ValueError("not a TIFF file") - self.pages = [] - while True: - try: - page = TiffPage(self) - self.pages.append(page) - except StopIteration: - break - if not self.pages: - raise ValueError("empty TIFF file") - - if self.is_micromanager: - # MicroManager files contain metadata not stored in TIFF tags. - self.micromanager_metadata = read_micromanager_metadata(self._fh) - - if self.is_lsm: - self._fix_lsm_strip_offsets() - self._fix_lsm_strip_byte_counts() - - def _fix_lsm_strip_offsets(self): - """Unwrap strip offsets for LSM files greater than 4 GB.""" - for series in self.series: - wrap = 0 - previous_offset = 0 - for page in series.pages: - strip_offsets = [] - for current_offset in page.strip_offsets: - if current_offset < previous_offset: - wrap += 2**32 - strip_offsets.append(current_offset + wrap) - previous_offset = current_offset - page.strip_offsets = tuple(strip_offsets) - - def _fix_lsm_strip_byte_counts(self): - """Set strip_byte_counts to size of compressed data. - - The strip_byte_counts tag in LSM files contains the number of bytes - for the uncompressed data. - - """ - if not self.pages: - return - strips = {} - for page in self.pages: - assert len(page.strip_offsets) == len(page.strip_byte_counts) - for offset, bytecount in zip(page.strip_offsets, - page.strip_byte_counts): - strips[offset] = bytecount - offsets = sorted(strips.keys()) - offsets.append(min(offsets[-1] + strips[offsets[-1]], self._fh.size)) - for i, offset in enumerate(offsets[:-1]): - strips[offset] = min(strips[offset], offsets[i+1] - offset) - for page in self.pages: - if page.compression: - page.strip_byte_counts = tuple( - strips[offset] for offset in page.strip_offsets) - - @lazyattr - def series(self): - """Return series of TiffPage with compatible shape and properties.""" - if not self.pages: - return [] - - series = [] - page0 = self.pages[0] - - if self.is_ome: - series = self._omeseries() - elif self.is_fluoview: - dims = {b'X': 'X', b'Y': 'Y', b'Z': 'Z', b'T': 'T', - b'WAVELENGTH': 'C', b'TIME': 'T', b'XY': 'R', - b'EVENT': 'V', b'EXPOSURE': 'L'} - mmhd = list(reversed(page0.mm_header.dimensions)) - series = [Record( - axes=''.join(dims.get(i[0].strip().upper(), 'Q') - for i in mmhd if i[1] > 1), - shape=tuple(int(i[1]) for i in mmhd if i[1] > 1), - pages=self.pages, dtype=numpy.dtype(page0.dtype))] - elif self.is_lsm: - lsmi = page0.cz_lsm_info - axes = CZ_SCAN_TYPES[lsmi.scan_type] - if page0.is_rgb: - axes = axes.replace('C', '').replace('XY', 'XYC') - axes = axes[::-1] - shape = tuple(getattr(lsmi, CZ_DIMENSIONS[i]) for i in axes) - pages = [p for p in self.pages if not p.is_reduced] - series = [Record(axes=axes, shape=shape, pages=pages, - dtype=numpy.dtype(pages[0].dtype))] - if len(pages) != len(self.pages): # reduced RGB pages - pages = [p for p in self.pages if p.is_reduced] - cp = 1 - i = 0 - while cp < len(pages) and i < len(shape)-2: - cp *= shape[i] - i += 1 - shape = shape[:i] + pages[0].shape - axes = axes[:i] + 'CYX' - series.append(Record(axes=axes, shape=shape, pages=pages, - dtype=numpy.dtype(pages[0].dtype))) - elif self.is_imagej: - shape = [] - axes = [] - ij = page0.imagej_tags - if 'frames' in ij: - shape.append(ij['frames']) - axes.append('T') - if 'slices' in ij: - shape.append(ij['slices']) - axes.append('Z') - if 'channels' in ij and not self.is_rgb: - shape.append(ij['channels']) - axes.append('C') - remain = len(self.pages) // (product(shape) if shape else 1) - if remain > 1: - shape.append(remain) - axes.append('I') - shape.extend(page0.shape) - axes.extend(page0.axes) - axes = ''.join(axes) - series = [Record(pages=self.pages, shape=tuple(shape), axes=axes, - dtype=numpy.dtype(page0.dtype))] - elif self.is_nih: - if len(self.pages) == 1: - shape = page0.shape - axes = page0.axes - else: - shape = (len(self.pages),) + page0.shape - axes = 'I' + page0.axes - series = [Record(pages=self.pages, shape=shape, axes=axes, - dtype=numpy.dtype(page0.dtype))] - elif page0.is_shaped: - # TODO: shaped files can contain multiple series - shape = page0.tags['image_description'].value[7:-1] - shape = tuple(int(i) for i in shape.split(b',')) - series = [Record(pages=self.pages, shape=shape, - axes='Q' * len(shape), - dtype=numpy.dtype(page0.dtype))] - - # generic detection of series - if not series: - shapes = [] - pages = {} - for page in self.pages: - if not page.shape: - continue - shape = page.shape + (page.axes, - page.compression in TIFF_DECOMPESSORS) - if shape not in pages: - shapes.append(shape) - pages[shape] = [page] - else: - pages[shape].append(page) - series = [Record(pages=pages[s], - axes=(('I' + s[-2]) - if len(pages[s]) > 1 else s[-2]), - dtype=numpy.dtype(pages[s][0].dtype), - shape=((len(pages[s]), ) + s[:-2] - if len(pages[s]) > 1 else s[:-2])) - for s in shapes] - - # remove empty series, e.g. in MD Gel files - series = [s for s in series if sum(s.shape) > 0] - - return series - - def asarray(self, key=None, series=None, memmap=False): - """Return image data from multiple TIFF pages as numpy array. - - By default the first image series is returned. - - Parameters - ---------- - key : int, slice, or sequence of page indices - Defines which pages to return as array. - series : int - Defines which series of pages to return as array. - memmap : bool - If True, return an array stored in a binary file on disk - if possible. - - """ - if key is None and series is None: - series = 0 - if series is not None: - pages = self.series[series].pages - else: - pages = self.pages - - if key is None: - pass - elif isinstance(key, int): - pages = [pages[key]] - elif isinstance(key, slice): - pages = pages[key] - elif isinstance(key, collections.Iterable): - pages = [pages[k] for k in key] - else: - raise TypeError("key must be an int, slice, or sequence") - - if not len(pages): - raise ValueError("no pages selected") - - if self.is_nih: - if pages[0].is_palette: - result = stack_pages(pages, colormapped=False, squeeze=False) - result = numpy.take(pages[0].color_map, result, axis=1) - result = numpy.swapaxes(result, 0, 1) - else: - result = stack_pages(pages, memmap=memmap, - colormapped=False, squeeze=False) - elif len(pages) == 1: - return pages[0].asarray(memmap=memmap) - elif self.is_ome: - assert not self.is_palette, "color mapping disabled for ome-tiff" - if any(p is None for p in pages): - # zero out missing pages - firstpage = next(p for p in pages if p) - nopage = numpy.zeros_like( - firstpage.asarray(memmap=False)) - s = self.series[series] - if memmap: - with tempfile.NamedTemporaryFile() as fh: - result = numpy.memmap(fh, dtype=s.dtype, shape=s.shape) - result = result.reshape(-1) - else: - result = numpy.empty(s.shape, s.dtype).reshape(-1) - index = 0 - - class KeepOpen: - # keep Tiff files open between consecutive pages - def __init__(self, parent, close): - self.master = parent - self.parent = parent - self._close = close - - def open(self, page): - if self._close and page and page.parent != self.parent: - if self.parent != self.master: - self.parent.filehandle.close() - self.parent = page.parent - self.parent.filehandle.open() - - def close(self): - if self._close and self.parent != self.master: - self.parent.filehandle.close() - - keep = KeepOpen(self, self._multifile_close) - for page in pages: - keep.open(page) - if page: - a = page.asarray(memmap=False, colormapped=False, - reopen=False) - else: - a = nopage - try: - result[index:index + a.size] = a.reshape(-1) - except ValueError as e: - warnings.warn("ome-tiff: %s" % e) - break - index += a.size - keep.close() - else: - result = stack_pages(pages, memmap=memmap) - - if key is None: - try: - result.shape = self.series[series].shape - except ValueError: - try: - warnings.warn("failed to reshape %s to %s" % ( - result.shape, self.series[series].shape)) - # try series of expected shapes - result.shape = (-1,) + self.series[series].shape - except ValueError: - # revert to generic shape - result.shape = (-1,) + pages[0].shape - else: - result.shape = (-1,) + pages[0].shape - return result - - def _omeseries(self): - """Return image series in OME-TIFF file(s).""" - root = etree.fromstring(self.pages[0].tags['image_description'].value) - uuid = root.attrib.get('UUID', None) - self._files = {uuid: self} - dirname = self._fh.dirname - modulo = {} - result = [] - for element in root: - if element.tag.endswith('BinaryOnly'): - warnings.warn("ome-xml: not an ome-tiff master file") - break - if element.tag.endswith('StructuredAnnotations'): - for annot in element: - if not annot.attrib.get('Namespace', - '').endswith('modulo'): - continue - for value in annot: - for modul in value: - for along in modul: - if not along.tag[:-1].endswith('Along'): - continue - axis = along.tag[-1] - newaxis = along.attrib.get('Type', 'other') - newaxis = AXES_LABELS[newaxis] - if 'Start' in along.attrib: - labels = range( - int(along.attrib['Start']), - int(along.attrib['End']) + 1, - int(along.attrib.get('Step', 1))) - else: - labels = [label.text for label in along - if label.tag.endswith('Label')] - modulo[axis] = (newaxis, labels) - if not element.tag.endswith('Image'): - continue - for pixels in element: - if not pixels.tag.endswith('Pixels'): - continue - atr = pixels.attrib - dtype = atr.get('Type', None) - axes = ''.join(reversed(atr['DimensionOrder'])) - shape = list(int(atr['Size'+ax]) for ax in axes) - size = product(shape[:-2]) - ifds = [None] * size - for data in pixels: - if not data.tag.endswith('TiffData'): - continue - atr = data.attrib - ifd = int(atr.get('IFD', 0)) - num = int(atr.get('NumPlanes', 1 if 'IFD' in atr else 0)) - num = int(atr.get('PlaneCount', num)) - idx = [int(atr.get('First'+ax, 0)) for ax in axes[:-2]] - try: - idx = numpy.ravel_multi_index(idx, shape[:-2]) - except ValueError: - # ImageJ produces invalid ome-xml when cropping - warnings.warn("ome-xml: invalid TiffData index") - continue - for uuid in data: - if not uuid.tag.endswith('UUID'): - continue - if uuid.text not in self._files: - if not self._multifile: - # abort reading multifile OME series - # and fall back to generic series - return [] - fname = uuid.attrib['FileName'] - try: - tif = TiffFile(os.path.join(dirname, fname)) - except (IOError, ValueError): - tif.close() - warnings.warn( - "ome-xml: failed to read '%s'" % fname) - break - self._files[uuid.text] = tif - if self._multifile_close: - tif.close() - pages = self._files[uuid.text].pages - try: - for i in range(num if num else len(pages)): - ifds[idx + i] = pages[ifd + i] - except IndexError: - warnings.warn("ome-xml: index out of range") - # only process first uuid - break - else: - pages = self.pages - try: - for i in range(num if num else len(pages)): - ifds[idx + i] = pages[ifd + i] - except IndexError: - warnings.warn("ome-xml: index out of range") - if all(i is None for i in ifds): - # skip images without data - continue - dtype = next(i for i in ifds if i).dtype - result.append(Record(axes=axes, shape=shape, pages=ifds, - dtype=numpy.dtype(dtype))) - - for record in result: - for axis, (newaxis, labels) in modulo.items(): - i = record.axes.index(axis) - size = len(labels) - if record.shape[i] == size: - record.axes = record.axes.replace(axis, newaxis, 1) - else: - record.shape[i] //= size - record.shape.insert(i+1, size) - record.axes = record.axes.replace(axis, axis+newaxis, 1) - record.shape = tuple(record.shape) - - # squeeze dimensions - for record in result: - record.shape, record.axes = squeeze_axes(record.shape, record.axes) - - return result - - def __len__(self): - """Return number of image pages in file.""" - return len(self.pages) - - def __getitem__(self, key): - """Return specified page.""" - return self.pages[key] - - def __iter__(self): - """Return iterator over pages.""" - return iter(self.pages) - - def __str__(self): - """Return string containing information about file.""" - result = [ - self._fh.name.capitalize(), - format_size(self._fh.size), - {'<': 'little endian', '>': 'big endian'}[self.byteorder]] - if self.is_bigtiff: - result.append("bigtiff") - if len(self.pages) > 1: - result.append("%i pages" % len(self.pages)) - if len(self.series) > 1: - result.append("%i series" % len(self.series)) - if len(self._files) > 1: - result.append("%i files" % (len(self._files))) - return ", ".join(result) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - @lazyattr - def fstat(self): - try: - return os.fstat(self._fh.fileno()) - except Exception: # io.UnsupportedOperation - return None - - @lazyattr - def is_bigtiff(self): - return self.offset_size != 4 - - @lazyattr - def is_rgb(self): - return all(p.is_rgb for p in self.pages) - - @lazyattr - def is_palette(self): - return all(p.is_palette for p in self.pages) - - @lazyattr - def is_mdgel(self): - return any(p.is_mdgel for p in self.pages) - - @lazyattr - def is_mediacy(self): - return any(p.is_mediacy for p in self.pages) - - @lazyattr - def is_stk(self): - return all(p.is_stk for p in self.pages) - - @lazyattr - def is_lsm(self): - return self.pages[0].is_lsm - - @lazyattr - def is_imagej(self): - return self.pages[0].is_imagej - - @lazyattr - def is_micromanager(self): - return self.pages[0].is_micromanager - - @lazyattr - def is_nih(self): - return self.pages[0].is_nih - - @lazyattr - def is_fluoview(self): - return self.pages[0].is_fluoview - - @lazyattr - def is_ome(self): - return self.pages[0].is_ome - - -class TiffPage(object): - """A TIFF image file directory (IFD). - - Attributes - ---------- - index : int - Index of page in file. - dtype : str {TIFF_SAMPLE_DTYPES} - Data type of image, colormapped if applicable. - shape : tuple - Dimensions of the image array in TIFF page, - colormapped and with one alpha channel if applicable. - axes : str - Axes label codes: - 'X' width, 'Y' height, 'S' sample, 'I' image series|page|plane, - 'Z' depth, 'C' color|em-wavelength|channel, 'E' ex-wavelength|lambda, - 'T' time, 'R' region|tile, 'A' angle, 'P' phase, 'H' lifetime, - 'L' exposure, 'V' event, 'Q' unknown, '_' missing - tags : TiffTags - Dictionary of tags in page. - Tag values are also directly accessible as attributes. - color_map : numpy array - Color look up table, if exists. - cz_lsm_scan_info: Record(dict) - LSM scan info attributes, if exists. - imagej_tags: Record(dict) - Consolidated ImageJ description and metadata tags, if exists. - uic_tags: Record(dict) - Consolidated MetaMorph STK/UIC tags, if exists. - - All attributes are read-only. - - Notes - ----- - The internal, normalized '_shape' attribute is 6 dimensional: - - 0. number planes (stk) - 1. planar samples_per_pixel - 2. image_depth Z (sgi) - 3. image_length Y - 4. image_width X - 5. contig samples_per_pixel - - """ - def __init__(self, parent): - """Initialize instance from file.""" - self.parent = parent - self.index = len(parent.pages) - self.shape = self._shape = () - self.dtype = self._dtype = None - self.axes = "" - self.tags = TiffTags() - - self._fromfile() - self._process_tags() - - def _fromfile(self): - """Read TIFF IFD structure and its tags from file. - - File cursor must be at storage position of IFD offset and is left at - offset to next IFD. - - Raises StopIteration if offset (first bytes read) is 0. - - """ - fh = self.parent.filehandle - byteorder = self.parent.byteorder - offset_size = self.parent.offset_size - - fmt = {4: 'I', 8: 'Q'}[offset_size] - offset = struct.unpack(byteorder + fmt, fh.read(offset_size))[0] - if not offset: - raise StopIteration() - - # read standard tags - tags = self.tags - fh.seek(offset) - fmt, size = {4: ('H', 2), 8: ('Q', 8)}[offset_size] - try: - numtags = struct.unpack(byteorder + fmt, fh.read(size))[0] - except Exception: - warnings.warn("corrupted page list") - raise StopIteration() - - tagcode = 0 - for _ in range(numtags): - try: - tag = TiffTag(self.parent) - # print(tag) - except TiffTag.Error as e: - warnings.warn(str(e)) - continue - if tagcode > tag.code: - # expected for early LSM and tifffile versions - warnings.warn("tags are not ordered by code") - tagcode = tag.code - if tag.name not in tags: - tags[tag.name] = tag - else: - # some files contain multiple IFD with same code - # e.g. MicroManager files contain two image_description - i = 1 - while True: - name = "%s_%i" % (tag.name, i) - if name not in tags: - tags[name] = tag - break - - pos = fh.tell() - - if self.is_lsm or (self.index and self.parent.is_lsm): - # correct non standard LSM bitspersample tags - self.tags['bits_per_sample']._correct_lsm_bitspersample(self) - - if self.is_lsm: - # read LSM info subrecords - for name, reader in CZ_LSM_INFO_READERS.items(): - try: - offset = self.cz_lsm_info['offset_'+name] - except KeyError: - continue - if offset < 8: - # older LSM revision - continue - fh.seek(offset) - try: - setattr(self, 'cz_lsm_'+name, reader(fh)) - except ValueError: - pass - - elif self.is_stk and 'uic1tag' in tags and not tags['uic1tag'].value: - # read uic1tag now that plane count is known - uic1tag = tags['uic1tag'] - fh.seek(uic1tag.value_offset) - tags['uic1tag'].value = Record( - read_uic1tag(fh, byteorder, uic1tag.dtype, uic1tag.count, - tags['uic2tag'].count)) - fh.seek(pos) - - def _process_tags(self): - """Validate standard tags and initialize attributes. - - Raise ValueError if tag values are not supported. - - """ - tags = self.tags - for code, (name, default, dtype, count, validate) in TIFF_TAGS.items(): - if not (name in tags or default is None): - tags[name] = TiffTag(code, dtype=dtype, count=count, - value=default, name=name) - if name in tags and validate: - try: - if tags[name].count == 1: - setattr(self, name, validate[tags[name].value]) - else: - setattr(self, name, tuple( - validate[value] for value in tags[name].value)) - except KeyError: - raise ValueError("%s.value (%s) not supported" % - (name, tags[name].value)) - - tag = tags['bits_per_sample'] - if tag.count == 1: - self.bits_per_sample = tag.value - else: - # LSM might list more items than samples_per_pixel - value = tag.value[:self.samples_per_pixel] - if any((v-value[0] for v in value)): - self.bits_per_sample = value - else: - self.bits_per_sample = value[0] - - tag = tags['sample_format'] - if tag.count == 1: - self.sample_format = TIFF_SAMPLE_FORMATS[tag.value] - else: - value = tag.value[:self.samples_per_pixel] - if any((v-value[0] for v in value)): - self.sample_format = [TIFF_SAMPLE_FORMATS[v] for v in value] - else: - self.sample_format = TIFF_SAMPLE_FORMATS[value[0]] - - if 'photometric' not in tags: - self.photometric = None - - if 'image_depth' not in tags: - self.image_depth = 1 - - if 'image_length' in tags: - self.strips_per_image = int(math.floor( - float(self.image_length + self.rows_per_strip - 1) / - self.rows_per_strip)) - else: - self.strips_per_image = 0 - - key = (self.sample_format, self.bits_per_sample) - self.dtype = self._dtype = TIFF_SAMPLE_DTYPES.get(key, None) - - if 'image_length' not in self.tags or 'image_width' not in self.tags: - # some GEL file pages are missing image data - self.image_length = 0 - self.image_width = 0 - self.image_depth = 0 - self.strip_offsets = 0 - self._shape = () - self.shape = () - self.axes = '' - - if self.is_palette: - self.dtype = self.tags['color_map'].dtype[1] - self.color_map = numpy.array(self.color_map, self.dtype) - dmax = self.color_map.max() - if dmax < 256: - self.dtype = numpy.uint8 - self.color_map = self.color_map.astype(self.dtype) - #else: - # self.dtype = numpy.uint8 - # self.color_map >>= 8 - # self.color_map = self.color_map.astype(self.dtype) - self.color_map.shape = (3, -1) - - # determine shape of data - image_length = self.image_length - image_width = self.image_width - image_depth = self.image_depth - samples_per_pixel = self.samples_per_pixel - - if self.is_stk: - assert self.image_depth == 1 - planes = self.tags['uic2tag'].count - if self.is_contig: - self._shape = (planes, 1, 1, image_length, image_width, - samples_per_pixel) - if samples_per_pixel == 1: - self.shape = (planes, image_length, image_width) - self.axes = 'YX' - else: - self.shape = (planes, image_length, image_width, - samples_per_pixel) - self.axes = 'YXS' - else: - self._shape = (planes, samples_per_pixel, 1, image_length, - image_width, 1) - if samples_per_pixel == 1: - self.shape = (planes, image_length, image_width) - self.axes = 'YX' - else: - self.shape = (planes, samples_per_pixel, image_length, - image_width) - self.axes = 'SYX' - # detect type of series - if planes == 1: - self.shape = self.shape[1:] - elif numpy.all(self.uic2tag.z_distance != 0): - self.axes = 'Z' + self.axes - elif numpy.all(numpy.diff(self.uic2tag.time_created) != 0): - self.axes = 'T' + self.axes - else: - self.axes = 'I' + self.axes - # DISABLED - if self.is_palette: - assert False, "color mapping disabled for stk" - if self.color_map.shape[1] >= 2**self.bits_per_sample: - if image_depth == 1: - self.shape = (3, planes, image_length, image_width) - else: - self.shape = (3, planes, image_depth, image_length, - image_width) - self.axes = 'C' + self.axes - else: - warnings.warn("palette cannot be applied") - self.is_palette = False - elif self.is_palette: - samples = 1 - if 'extra_samples' in self.tags: - samples += len(self.extra_samples) - if self.is_contig: - self._shape = (1, 1, image_depth, image_length, image_width, - samples) - else: - self._shape = (1, samples, image_depth, image_length, - image_width, 1) - if self.color_map.shape[1] >= 2**self.bits_per_sample: - if image_depth == 1: - self.shape = (3, image_length, image_width) - self.axes = 'CYX' - else: - self.shape = (3, image_depth, image_length, image_width) - self.axes = 'CZYX' - else: - warnings.warn("palette cannot be applied") - self.is_palette = False - if image_depth == 1: - self.shape = (image_length, image_width) - self.axes = 'YX' - else: - self.shape = (image_depth, image_length, image_width) - self.axes = 'ZYX' - elif self.is_rgb or samples_per_pixel > 1: - if self.is_contig: - self._shape = (1, 1, image_depth, image_length, image_width, - samples_per_pixel) - if image_depth == 1: - self.shape = (image_length, image_width, samples_per_pixel) - self.axes = 'YXS' - else: - self.shape = (image_depth, image_length, image_width, - samples_per_pixel) - self.axes = 'ZYXS' - else: - self._shape = (1, samples_per_pixel, image_depth, - image_length, image_width, 1) - if image_depth == 1: - self.shape = (samples_per_pixel, image_length, image_width) - self.axes = 'SYX' - else: - self.shape = (samples_per_pixel, image_depth, - image_length, image_width) - self.axes = 'SZYX' - if False and self.is_rgb and 'extra_samples' in self.tags: - # DISABLED: only use RGB and first alpha channel if exists - extra_samples = self.extra_samples - if self.tags['extra_samples'].count == 1: - extra_samples = (extra_samples, ) - for exs in extra_samples: - if exs in ('unassalpha', 'assocalpha', 'unspecified'): - if self.is_contig: - self.shape = self.shape[:-1] + (4,) - else: - self.shape = (4,) + self.shape[1:] - break - else: - self._shape = (1, 1, image_depth, image_length, image_width, 1) - if image_depth == 1: - self.shape = (image_length, image_width) - self.axes = 'YX' - else: - self.shape = (image_depth, image_length, image_width) - self.axes = 'ZYX' - if not self.compression and 'strip_byte_counts' not in tags: - self.strip_byte_counts = ( - product(self.shape) * (self.bits_per_sample // 8), ) - - assert len(self.shape) == len(self.axes) - - def asarray(self, squeeze=True, colormapped=True, rgbonly=False, - scale_mdgel=False, memmap=False, reopen=True): - """Read image data from file and return as numpy array. - - Raise ValueError if format is unsupported. - If any of 'squeeze', 'colormapped', or 'rgbonly' are not the default, - the shape of the returned array might be different from the page shape. - - Parameters - ---------- - squeeze : bool - If True, all length-1 dimensions (except X and Y) are - squeezed out from result. - colormapped : bool - If True, color mapping is applied for palette-indexed images. - rgbonly : bool - If True, return RGB(A) image without additional extra samples. - memmap : bool - If True, use numpy.memmap to read arrays from file if possible. - For use on 64 bit systems and files with few huge contiguous data. - reopen : bool - If True and the parent file handle is closed, the file is - temporarily re-opened (and closed if no exception occurs). - scale_mdgel : bool - If True, MD Gel data will be scaled according to the private - metadata in the second TIFF page. The dtype will be float32. - - """ - if not self._shape: - return - - if self.dtype is None: - raise ValueError("data type not supported: %s%i" % ( - self.sample_format, self.bits_per_sample)) - if self.compression not in TIFF_DECOMPESSORS: - raise ValueError("cannot decompress %s" % self.compression) - tag = self.tags['sample_format'] - if tag.count != 1 and any((i-tag.value[0] for i in tag.value)): - raise ValueError("sample formats don't match %s" % str(tag.value)) - - fh = self.parent.filehandle - closed = fh.closed - if closed: - if reopen: - fh.open() - else: - raise IOError("file handle is closed") - - dtype = self._dtype - shape = self._shape - image_width = self.image_width - image_length = self.image_length - image_depth = self.image_depth - typecode = self.parent.byteorder + dtype - bits_per_sample = self.bits_per_sample - - if self.is_tiled: - if 'tile_offsets' in self.tags: - byte_counts = self.tile_byte_counts - offsets = self.tile_offsets - else: - byte_counts = self.strip_byte_counts - offsets = self.strip_offsets - tile_width = self.tile_width - tile_length = self.tile_length - tile_depth = self.tile_depth if 'tile_depth' in self.tags else 1 - tw = (image_width + tile_width - 1) // tile_width - tl = (image_length + tile_length - 1) // tile_length - td = (image_depth + tile_depth - 1) // tile_depth - shape = (shape[0], shape[1], - td*tile_depth, tl*tile_length, tw*tile_width, shape[-1]) - tile_shape = (tile_depth, tile_length, tile_width, shape[-1]) - runlen = tile_width - else: - byte_counts = self.strip_byte_counts - offsets = self.strip_offsets - runlen = image_width - - if any(o < 2 for o in offsets): - raise ValueError("corrupted page") - - if memmap and self._is_memmappable(rgbonly, colormapped): - result = fh.memmap_array(typecode, shape, offset=offsets[0]) - elif self.is_contiguous: - fh.seek(offsets[0]) - result = fh.read_array(typecode, product(shape)) - result = result.astype('=' + dtype) - else: - if self.is_contig: - runlen *= self.samples_per_pixel - if bits_per_sample in (8, 16, 32, 64, 128): - if (bits_per_sample * runlen) % 8: - raise ValueError("data and sample size mismatch") - - def unpack(x): - try: - return numpy.fromstring(x, typecode) - except ValueError as e: - # strips may be missing EOI - warnings.warn("unpack: %s" % e) - xlen = ((len(x) // (bits_per_sample // 8)) - * (bits_per_sample // 8)) - return numpy.fromstring(x[:xlen], typecode) - - elif isinstance(bits_per_sample, tuple): - def unpack(x): - return unpackrgb(x, typecode, bits_per_sample) - else: - def unpack(x): - return unpackints(x, typecode, bits_per_sample, runlen) - - decompress = TIFF_DECOMPESSORS[self.compression] - if self.compression == 'jpeg': - table = self.jpeg_tables if 'jpeg_tables' in self.tags else b'' - decompress = lambda x: decodejpg(x, table, self.photometric) - - if self.is_tiled: - result = numpy.empty(shape, dtype) - tw, tl, td, pl = 0, 0, 0, 0 - for offset, bytecount in zip(offsets, byte_counts): - fh.seek(offset) - tile = unpack(decompress(fh.read(bytecount))) - tile.shape = tile_shape - if self.predictor == 'horizontal': - numpy.cumsum(tile, axis=-2, dtype=dtype, out=tile) - result[0, pl, td:td+tile_depth, - tl:tl+tile_length, tw:tw+tile_width, :] = tile - del tile - tw += tile_width - if tw >= shape[4]: - tw, tl = 0, tl + tile_length - if tl >= shape[3]: - tl, td = 0, td + tile_depth - if td >= shape[2]: - td, pl = 0, pl + 1 - result = result[..., - :image_depth, :image_length, :image_width, :] - else: - strip_size = (self.rows_per_strip * self.image_width * - self.samples_per_pixel) - result = numpy.empty(shape, dtype).reshape(-1) - index = 0 - for offset, bytecount in zip(offsets, byte_counts): - fh.seek(offset) - strip = fh.read(bytecount) - strip = decompress(strip) - strip = unpack(strip) - size = min(result.size, strip.size, strip_size, - result.size - index) - result[index:index+size] = strip[:size] - del strip - index += size - - result.shape = self._shape - - if self.predictor == 'horizontal' and not (self.is_tiled and not - self.is_contiguous): - # work around bug in LSM510 software - if not (self.parent.is_lsm and not self.compression): - numpy.cumsum(result, axis=-2, dtype=dtype, out=result) - - if colormapped and self.is_palette: - if self.color_map.shape[1] >= 2**bits_per_sample: - # FluoView and LSM might fail here - result = numpy.take(self.color_map, - result[:, 0, :, :, :, 0], axis=1) - elif rgbonly and self.is_rgb and 'extra_samples' in self.tags: - # return only RGB and first alpha channel if exists - extra_samples = self.extra_samples - if self.tags['extra_samples'].count == 1: - extra_samples = (extra_samples, ) - for i, exs in enumerate(extra_samples): - if exs in ('unassalpha', 'assocalpha', 'unspecified'): - if self.is_contig: - result = result[..., [0, 1, 2, 3+i]] - else: - result = result[:, [0, 1, 2, 3+i]] - break - else: - if self.is_contig: - result = result[..., :3] - else: - result = result[:, :3] - - if squeeze: - try: - result.shape = self.shape - except ValueError: - warnings.warn("failed to reshape from %s to %s" % ( - str(result.shape), str(self.shape))) - - if scale_mdgel and self.parent.is_mdgel: - # MD Gel stores private metadata in the second page - tags = self.parent.pages[1] - if tags.md_file_tag in (2, 128): - scale = tags.md_scale_pixel - scale = scale[0] / scale[1] # rational - result = result.astype('float32') - if tags.md_file_tag == 2: - result **= 2 # squary root data format - result *= scale - - if closed: - # TODO: file remains open if an exception occurred above - fh.close() - return result - - def _is_memmappable(self, rgbonly, colormapped): - """Return if image data in file can be memory mapped.""" - if not self.parent.filehandle.is_file or not self.is_contiguous: - return False - return not (self.predictor or - (rgbonly and 'extra_samples' in self.tags) or - (colormapped and self.is_palette) or - ({'big': '>', 'little': '<'}[sys.byteorder] != - self.parent.byteorder)) - - @lazyattr - def is_contiguous(self): - """Return offset and size of contiguous data, else None. - - Excludes prediction and colormapping. - - """ - if self.compression or self.bits_per_sample not in (8, 16, 32, 64): - return - if self.is_tiled: - if (self.image_width != self.tile_width or - self.image_length % self.tile_length or - self.tile_width % 16 or self.tile_length % 16): - return - if ('image_depth' in self.tags and 'tile_depth' in self.tags and - (self.image_length != self.tile_length or - self.image_depth % self.tile_depth)): - return - offsets = self.tile_offsets - byte_counts = self.tile_byte_counts - else: - offsets = self.strip_offsets - byte_counts = self.strip_byte_counts - if len(offsets) == 1: - return offsets[0], byte_counts[0] - if self.is_stk or all(offsets[i] + byte_counts[i] == offsets[i+1] - or byte_counts[i+1] == 0 # no data/ignore offset - for i in range(len(offsets)-1)): - return offsets[0], sum(byte_counts) - - def __str__(self): - """Return string containing information about page.""" - s = ', '.join(s for s in ( - ' x '.join(str(i) for i in self.shape), - str(numpy.dtype(self.dtype)), - '%s bit' % str(self.bits_per_sample), - self.photometric if 'photometric' in self.tags else '', - self.compression if self.compression else 'raw', - '|'.join(t[3:] for t in ( - 'is_stk', 'is_lsm', 'is_nih', 'is_ome', 'is_imagej', - 'is_micromanager', 'is_fluoview', 'is_mdgel', 'is_mediacy', - 'is_sgi', 'is_reduced', 'is_tiled', - 'is_contiguous') if getattr(self, t))) if s) - return "Page %i: %s" % (self.index, s) - - def __getattr__(self, name): - """Return tag value.""" - if name in self.tags: - value = self.tags[name].value - setattr(self, name, value) - return value - raise AttributeError(name) - - @lazyattr - def uic_tags(self): - """Consolidate UIC tags.""" - if not self.is_stk: - raise AttributeError("uic_tags") - tags = self.tags - result = Record() - result.number_planes = tags['uic2tag'].count - if 'image_description' in tags: - result.plane_descriptions = self.image_description.split(b'\x00') - if 'uic1tag' in tags: - result.update(tags['uic1tag'].value) - if 'uic3tag' in tags: - result.update(tags['uic3tag'].value) # wavelengths - if 'uic4tag' in tags: - result.update(tags['uic4tag'].value) # override uic1 tags - uic2tag = tags['uic2tag'].value - result.z_distance = uic2tag.z_distance - result.time_created = uic2tag.time_created - result.time_modified = uic2tag.time_modified - try: - result.datetime_created = [ - julian_datetime(*dt) for dt in - zip(uic2tag.date_created, uic2tag.time_created)] - result.datetime_modified = [ - julian_datetime(*dt) for dt in - zip(uic2tag.date_modified, uic2tag.time_modified)] - except ValueError as e: - warnings.warn("uic_tags: %s" % e) - return result - - @lazyattr - def imagej_tags(self): - """Consolidate ImageJ metadata.""" - if not self.is_imagej: - raise AttributeError("imagej_tags") - tags = self.tags - if 'image_description_1' in tags: - # MicroManager - result = imagej_description(tags['image_description_1'].value) - else: - result = imagej_description(tags['image_description'].value) - if 'imagej_metadata' in tags: - try: - result.update(imagej_metadata( - tags['imagej_metadata'].value, - tags['imagej_byte_counts'].value, - self.parent.byteorder)) - except Exception as e: - warnings.warn(str(e)) - return Record(result) - - @lazyattr - def is_rgb(self): - """True if page contains a RGB image.""" - return ('photometric' in self.tags and - self.tags['photometric'].value == 2) - - @lazyattr - def is_contig(self): - """True if page contains a contiguous image.""" - return ('planar_configuration' in self.tags and - self.tags['planar_configuration'].value == 1) - - @lazyattr - def is_palette(self): - """True if page contains a palette-colored image and not OME or STK.""" - try: - # turn off color mapping for OME-TIFF and STK - if self.is_stk or self.is_ome or self.parent.is_ome: - return False - except IndexError: - pass # OME-XML not found in first page - return ('photometric' in self.tags and - self.tags['photometric'].value == 3) - - @lazyattr - def is_tiled(self): - """True if page contains tiled image.""" - return 'tile_width' in self.tags - - @lazyattr - def is_reduced(self): - """True if page is a reduced image of another image.""" - return bool(self.tags['new_subfile_type'].value & 1) - - @lazyattr - def is_mdgel(self): - """True if page contains md_file_tag tag.""" - return 'md_file_tag' in self.tags - - @lazyattr - def is_mediacy(self): - """True if page contains Media Cybernetics Id tag.""" - return ('mc_id' in self.tags and - self.tags['mc_id'].value.startswith(b'MC TIFF')) - - @lazyattr - def is_stk(self): - """True if page contains UIC2Tag tag.""" - return 'uic2tag' in self.tags - - @lazyattr - def is_lsm(self): - """True if page contains LSM CZ_LSM_INFO tag.""" - return 'cz_lsm_info' in self.tags - - @lazyattr - def is_fluoview(self): - """True if page contains FluoView MM_STAMP tag.""" - return 'mm_stamp' in self.tags - - @lazyattr - def is_nih(self): - """True if page contains NIH image header.""" - return 'nih_image_header' in self.tags - - @lazyattr - def is_sgi(self): - """True if page contains SGI image and tile depth tags.""" - return 'image_depth' in self.tags and 'tile_depth' in self.tags - - @lazyattr - def is_ome(self): - """True if page contains OME-XML in image_description tag.""" - return ('image_description' in self.tags and self.tags[ - 'image_description'].value.startswith(b' parent.offset_size or code in CUSTOM_TAGS: - pos = fh.tell() - tof = {4: 'I', 8: 'Q'}[parent.offset_size] - self.value_offset = offset = struct.unpack(byteorder+tof, value)[0] - if offset < 0 or offset > parent.filehandle.size: - raise TiffTag.Error("corrupt file - invalid tag value offset") - elif offset < 4: - raise TiffTag.Error("corrupt value offset for tag %i" % code) - fh.seek(offset) - if code in CUSTOM_TAGS: - readfunc = CUSTOM_TAGS[code][1] - value = readfunc(fh, byteorder, dtype, count) - if isinstance(value, dict): # numpy.core.records.record - value = Record(value) - elif code in TIFF_TAGS or dtype[-1] == 's': - value = struct.unpack(fmt, fh.read(size)) - else: - value = read_numpy(fh, byteorder, dtype, count) - fh.seek(pos) - else: - value = struct.unpack(fmt, value[:size]) - - if code not in CUSTOM_TAGS and code not in (273, 279, 324, 325): - # scalar value if not strip/tile offsets/byte_counts - if len(value) == 1: - value = value[0] - - if (dtype.endswith('s') and isinstance(value, bytes) - and self._type != 7): - # TIFF ASCII fields can contain multiple strings, - # each terminated with a NUL - value = stripascii(value) - - self.code = code - self.name = name - self.dtype = dtype - self.count = count - self.value = value - - def _correct_lsm_bitspersample(self, parent): - """Correct LSM bitspersample tag. - - Old LSM writers may use a separate region for two 16-bit values, - although they fit into the tag value element of the tag. - - """ - if self.code == 258 and self.count == 2: - # TODO: test this. Need example file. - warnings.warn("correcting LSM bitspersample tag") - fh = parent.filehandle - tof = {4: '') - - def __str__(self): - """Return string containing information about tag.""" - return ' '.join(str(getattr(self, s)) for s in self.__slots__) - - -class TiffSequence(object): - """Sequence of image files. - - The data shape and dtype of all files must match. - - Properties - ---------- - files : list - List of file names. - shape : tuple - Shape of image sequence. - axes : str - Labels of axes in shape. - - Examples - -------- - >>> tifs = TiffSequence("test.oif.files/*.tif") - >>> tifs.shape, tifs.axes - ((2, 100), 'CT') - >>> data = tifs.asarray() - >>> data.shape - (2, 100, 256, 256) - - """ - _patterns = { - 'axes': r""" - # matches Olympus OIF and Leica TIFF series - _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4})) - _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? - _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? - _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? - _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? - _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? - _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? - """} - - class ParseError(Exception): - pass - - def __init__(self, files, imread=TiffFile, pattern='axes', - *args, **kwargs): - """Initialize instance from multiple files. - - Parameters - ---------- - files : str, or sequence of str - Glob pattern or sequence of file names. - imread : function or class - Image read function or class with asarray function returning numpy - array from single file. - pattern : str - Regular expression pattern that matches axes names and sequence - indices in file names. - By default this matches Olympus OIF and Leica TIFF series. - - """ - if isinstance(files, basestring): - files = natural_sorted(glob.glob(files)) - files = list(files) - if not files: - raise ValueError("no files found") - #if not os.path.isfile(files[0]): - # raise ValueError("file not found") - self.files = files - - if hasattr(imread, 'asarray'): - # redefine imread - _imread = imread - - def imread(fname, *args, **kwargs): - with _imread(fname) as im: - return im.asarray(*args, **kwargs) - - self.imread = imread - - self.pattern = self._patterns.get(pattern, pattern) - try: - self._parse() - if not self.axes: - self.axes = 'I' - except self.ParseError: - self.axes = 'I' - self.shape = (len(files),) - self._start_index = (0,) - self._indices = tuple((i,) for i in range(len(files))) - - def __str__(self): - """Return string with information about image sequence.""" - return "\n".join([ - self.files[0], - '* files: %i' % len(self.files), - '* axes: %s' % self.axes, - '* shape: %s' % str(self.shape)]) - - def __len__(self): - return len(self.files) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - def close(self): - pass - - def asarray(self, memmap=False, *args, **kwargs): - """Read image data from all files and return as single numpy array. - - If memmap is True, return an array stored in a binary file on disk. - The args and kwargs parameters are passed to the imread function. - - Raise IndexError or ValueError if image shapes don't match. - - """ - im = self.imread(self.files[0], *args, **kwargs) - shape = self.shape + im.shape - if memmap: - with tempfile.NamedTemporaryFile() as fh: - result = numpy.memmap(fh, dtype=im.dtype, shape=shape) - else: - result = numpy.zeros(shape, dtype=im.dtype) - result = result.reshape(-1, *im.shape) - for index, fname in zip(self._indices, self.files): - index = [i-j for i, j in zip(index, self._start_index)] - index = numpy.ravel_multi_index(index, self.shape) - im = self.imread(fname, *args, **kwargs) - result[index] = im - result.shape = shape - return result - - def _parse(self): - """Get axes and shape from file names.""" - if not self.pattern: - raise self.ParseError("invalid pattern") - pattern = re.compile(self.pattern, re.IGNORECASE | re.VERBOSE) - matches = pattern.findall(self.files[0]) - if not matches: - raise self.ParseError("pattern doesn't match file names") - matches = matches[-1] - if len(matches) % 2: - raise self.ParseError("pattern doesn't match axis name and index") - axes = ''.join(m for m in matches[::2] if m) - if not axes: - raise self.ParseError("pattern doesn't match file names") - - indices = [] - for fname in self.files: - matches = pattern.findall(fname)[-1] - if axes != ''.join(m for m in matches[::2] if m): - raise ValueError("axes don't match within the image sequence") - indices.append([int(m) for m in matches[1::2] if m]) - shape = tuple(numpy.max(indices, axis=0)) - start_index = tuple(numpy.min(indices, axis=0)) - shape = tuple(i-j+1 for i, j in zip(shape, start_index)) - if product(shape) != len(self.files): - warnings.warn("files are missing. Missing data are zeroed") - - self.axes = axes.upper() - self.shape = shape - self._indices = indices - self._start_index = start_index - - -class Record(dict): - """Dictionary with attribute access. - - Can also be initialized with numpy.core.records.record. - - """ - __slots__ = () - - def __init__(self, arg=None, **kwargs): - if kwargs: - arg = kwargs - elif arg is None: - arg = {} - try: - dict.__init__(self, arg) - except (TypeError, ValueError): - for i, name in enumerate(arg.dtype.names): - v = arg[i] - self[name] = v if v.dtype.char != 'S' else stripnull(v) - - def __getattr__(self, name): - return self[name] - - def __setattr__(self, name, value): - self.__setitem__(name, value) - - def __str__(self): - """Pretty print Record.""" - s = [] - lists = [] - for k in sorted(self): - try: - if k.startswith('_'): # does not work with byte - continue - except AttributeError: - pass - v = self[k] - if isinstance(v, (list, tuple)) and len(v): - if isinstance(v[0], Record): - lists.append((k, v)) - continue - elif isinstance(v[0], TiffPage): - v = [i.index for i in v if i] - s.append( - ("* %s: %s" % (k, str(v))).split("\n", 1)[0] - [:PRINT_LINE_LEN].rstrip()) - for k, v in lists: - l = [] - for i, w in enumerate(v): - l.append("* %s[%i]\n %s" % (k, i, - str(w).replace("\n", "\n "))) - s.append('\n'.join(l)) - return '\n'.join(s) - - -class TiffTags(Record): - """Dictionary of TiffTag with attribute access.""" - - def __str__(self): - """Return string with information about all tags.""" - s = [] - for tag in sorted(self.values(), key=lambda x: x.code): - typecode = "%i%s" % (tag.count * int(tag.dtype[0]), tag.dtype[1]) - line = "* %i %s (%s) %s" % ( - tag.code, tag.name, typecode, tag.as_str()) - s.append(line[:PRINT_LINE_LEN].lstrip()) - return '\n'.join(s) - - -class FileHandle(object): - """Binary file handle. - - * Handle embedded files (for CZI within CZI files). - * Allow to re-open closed files (for multi file formats such as OME-TIFF). - * Read numpy arrays and records from file like objects. - - Only binary read, seek, tell, and close are supported on embedded files. - When initialized from another file handle, do not use it unless this - FileHandle is closed. - - Attributes - ---------- - name : str - Name of the file. - path : str - Absolute path to file. - size : int - Size of file in bytes. - is_file : bool - If True, file has a filno and can be memory mapped. - - All attributes are read-only. - - """ - __slots__ = ('_fh', '_arg', '_mode', '_name', '_dir', - '_offset', '_size', '_close', 'is_file') - - def __init__(self, arg, mode='rb', name=None, offset=None, size=None): - """Initialize file handle from file name or another file handle. - - Parameters - ---------- - arg : str, File, or FileHandle - File name or open file handle. - mode : str - File open mode in case 'arg' is a file name. - name : str - Optional name of file in case 'arg' is a file handle. - offset : int - Optional start position of embedded file. By default this is - the current file position. - size : int - Optional size of embedded file. By default this is the number - of bytes from the 'offset' to the end of the file. - - """ - self._fh = None - self._arg = arg - self._mode = mode - self._name = name - self._dir = '' - self._offset = offset - self._size = size - self._close = True - self.is_file = False - self.open() - - def open(self): - """Open or re-open file.""" - if self._fh: - return # file is open - - if isinstance(self._arg, basestring): - # file name - self._arg = os.path.abspath(self._arg) - self._dir, self._name = os.path.split(self._arg) - self._fh = open(self._arg, self._mode) - self._close = True - if self._offset is None: - self._offset = 0 - elif isinstance(self._arg, FileHandle): - # FileHandle - self._fh = self._arg._fh - if self._offset is None: - self._offset = 0 - self._offset += self._arg._offset - self._close = False - if not self._name: - if self._offset: - name, ext = os.path.splitext(self._arg._name) - self._name = "%s@%i%s" % (name, self._offset, ext) - else: - self._name = self._arg._name - self._dir = self._arg._dir - else: - # open file object - self._fh = self._arg - if self._offset is None: - self._offset = self._arg.tell() - self._close = False - if not self._name: - try: - self._dir, self._name = os.path.split(self._fh.name) - except AttributeError: - self._name = "Unnamed stream" - - if self._offset: - self._fh.seek(self._offset) - - if self._size is None: - pos = self._fh.tell() - self._fh.seek(self._offset, 2) - self._size = self._fh.tell() - self._fh.seek(pos) - - try: - self._fh.fileno() - self.is_file = True - except Exception: - self.is_file = False - - def read(self, size=-1): - """Read 'size' bytes from file, or until EOF is reached.""" - if size < 0 and self._offset: - size = self._size - return self._fh.read(size) - - def memmap_array(self, dtype, shape, offset=0, mode='r', order='C'): - """Return numpy.memmap of data stored in file.""" - if not self.is_file: - raise ValueError("Can not memory map file without fileno.") - return numpy.memmap(self._fh, dtype=dtype, mode=mode, - offset=self._offset + offset, - shape=shape, order=order) - - def read_array(self, dtype, count=-1, sep=""): - """Return numpy array from file. - - Work around numpy issue #2230, "numpy.fromfile does not accept - StringIO object" https://github.com/numpy/numpy/issues/2230. - - """ - try: - return numpy.fromfile(self._fh, dtype, count, sep) - except IOError: - if count < 0: - size = self._size - else: - size = count * numpy.dtype(dtype).itemsize - data = self._fh.read(size) - return numpy.fromstring(data, dtype, count, sep) - - def read_record(self, dtype, shape=1, byteorder=None): - """Return numpy record from file.""" - try: - rec = numpy.rec.fromfile(self._fh, dtype, shape, - byteorder=byteorder) - except Exception: - dtype = numpy.dtype(dtype) - if shape is None: - shape = self._size // dtype.itemsize - size = product(sequence(shape)) * dtype.itemsize - data = self._fh.read(size) - return numpy.rec.fromstring(data, dtype, shape, - byteorder=byteorder) - return rec[0] if shape == 1 else rec - - def tell(self): - """Return file's current position.""" - return self._fh.tell() - self._offset - - def seek(self, offset, whence=0): - """Set file's current position.""" - if self._offset: - if whence == 0: - self._fh.seek(self._offset + offset, whence) - return - elif whence == 2: - self._fh.seek(self._offset + self._size + offset, 0) - return - self._fh.seek(offset, whence) - - def close(self): - """Close file.""" - if self._close and self._fh: - self._fh.close() - self._fh = None - self.is_file = False - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - def __getattr__(self, name): - """Return attribute from underlying file object.""" - if self._offset: - warnings.warn( - "FileHandle: '%s' not implemented for embedded files" % name) - return getattr(self._fh, name) - - @property - def name(self): - return self._name - - @property - def dirname(self): - return self._dir - - @property - def path(self): - return os.path.join(self._dir, self._name) - - @property - def size(self): - return self._size - - @property - def closed(self): - return self._fh is None - - -def read_bytes(fh, byteorder, dtype, count): - """Read tag data from file and return as byte string.""" - dtype = 'b' if dtype[-1] == 's' else byteorder+dtype[-1] - return fh.read_array(dtype, count).tostring() - - -def read_numpy(fh, byteorder, dtype, count): - """Read tag data from file and return as numpy array.""" - dtype = 'b' if dtype[-1] == 's' else byteorder+dtype[-1] - return fh.read_array(dtype, count) - - -def read_json(fh, byteorder, dtype, count): - """Read JSON tag data from file and return as object.""" - data = fh.read(count) - try: - return json.loads(unicode(stripnull(data), 'utf-8')) - except ValueError: - warnings.warn("invalid JSON `%s`" % data) - - -def read_mm_header(fh, byteorder, dtype, count): - """Read MM_HEADER tag from file and return as numpy.rec.array.""" - return fh.read_record(MM_HEADER, byteorder=byteorder) - - -def read_mm_stamp(fh, byteorder, dtype, count): - """Read MM_STAMP tag from file and return as numpy.array.""" - return fh.read_array(byteorder+'f8', 8) - - -def read_uic1tag(fh, byteorder, dtype, count, plane_count=None): - """Read MetaMorph STK UIC1Tag from file and return as dictionary. - - Return empty dictionary if plane_count is unknown. - - """ - assert dtype in ('2I', '1I') and byteorder == '<' - result = {} - if dtype == '2I': - # pre MetaMorph 2.5 (not tested) - values = fh.read_array(' structure_size: - break - cz_lsm_info.append((name, dtype)) - else: - cz_lsm_info = CZ_LSM_INFO - - return fh.read_record(cz_lsm_info, byteorder=byteorder) - - -def read_cz_lsm_floatpairs(fh): - """Read LSM sequence of float pairs from file and return as list.""" - size = struct.unpack(' 0: - esize, etime, etype = struct.unpack(''}[fh.read(2)] - except IndexError: - raise ValueError("not a MicroManager TIFF file") - - results = {} - fh.seek(8) - (index_header, index_offset, display_header, display_offset, - comments_header, comments_offset, summary_header, summary_length - ) = struct.unpack(byteorder + "IIIIIIII", fh.read(32)) - - if summary_header != 2355492: - raise ValueError("invalid MicroManager summary_header") - results['summary'] = read_json(fh, byteorder, None, summary_length) - - if index_header != 54773648: - raise ValueError("invalid MicroManager index_header") - fh.seek(index_offset) - header, count = struct.unpack(byteorder + "II", fh.read(8)) - if header != 3453623: - raise ValueError("invalid MicroManager index_header") - data = struct.unpack(byteorder + "IIIII"*count, fh.read(20*count)) - results['index_map'] = { - 'channel': data[::5], 'slice': data[1::5], 'frame': data[2::5], - 'position': data[3::5], 'offset': data[4::5]} - - if display_header != 483765892: - raise ValueError("invalid MicroManager display_header") - fh.seek(display_offset) - header, count = struct.unpack(byteorder + "II", fh.read(8)) - if header != 347834724: - raise ValueError("invalid MicroManager display_header") - results['display_settings'] = read_json(fh, byteorder, None, count) - - if comments_header != 99384722: - raise ValueError("invalid MicroManager comments_header") - fh.seek(comments_offset) - header, count = struct.unpack(byteorder + "II", fh.read(8)) - if header != 84720485: - raise ValueError("invalid MicroManager comments_header") - results['comments'] = read_json(fh, byteorder, None, count) - - return results - - -def imagej_metadata(data, bytecounts, byteorder): - """Return dict from ImageJ metadata tag value.""" - _str = str if sys.version_info[0] < 3 else lambda x: str(x, 'cp1252') - - def read_string(data, byteorder): - return _str(stripnull(data[0 if byteorder == '<' else 1::2])) - - def read_double(data, byteorder): - return struct.unpack(byteorder+('d' * (len(data) // 8)), data) - - def read_bytes(data, byteorder): - #return struct.unpack('b' * len(data), data) - return numpy.fromstring(data, 'uint8') - - metadata_types = { # big endian - b'info': ('info', read_string), - b'labl': ('labels', read_string), - b'rang': ('ranges', read_double), - b'luts': ('luts', read_bytes), - b'roi ': ('roi', read_bytes), - b'over': ('overlays', read_bytes)} - metadata_types.update( # little endian - dict((k[::-1], v) for k, v in metadata_types.items())) - - if not bytecounts: - raise ValueError("no ImageJ metadata") - - if not data[:4] in (b'IJIJ', b'JIJI'): - raise ValueError("invalid ImageJ metadata") - - header_size = bytecounts[0] - if header_size < 12 or header_size > 804: - raise ValueError("invalid ImageJ metadata header size") - - ntypes = (header_size - 4) // 8 - header = struct.unpack(byteorder+'4sI'*ntypes, data[4:4+ntypes*8]) - pos = 4 + ntypes * 8 - counter = 0 - result = {} - for mtype, count in zip(header[::2], header[1::2]): - values = [] - name, func = metadata_types.get(mtype, (_str(mtype), read_bytes)) - for _ in range(count): - counter += 1 - pos1 = pos + bytecounts[counter] - values.append(func(data[pos:pos1], byteorder)) - pos = pos1 - result[name.strip()] = values[0] if count == 1 else values - return result - - -def imagej_description(description): - """Return dict from ImageJ image_description tag.""" - def _bool(val): - return {b'true': True, b'false': False}[val.lower()] - - _str = str if sys.version_info[0] < 3 else lambda x: str(x, 'cp1252') - result = {} - for line in description.splitlines(): - try: - key, val = line.split(b'=') - except Exception: - continue - key = key.strip() - val = val.strip() - for dtype in (int, float, _bool, _str): - try: - val = dtype(val) - break - except Exception: - pass - result[_str(key)] = val - return result - - -def _replace_by(module_function, package=None, warn=False): - """Try replace decorated function by module.function.""" - try: - from importlib import import_module - except ImportError: - warnings.warn('could not import module importlib') - return lambda func: func - - def decorate(func, module_function=module_function, warn=warn): - try: - module, function = module_function.split('.') - if not package: - module = import_module(module) - else: - module = import_module('.' + module, package=package) - func, oldfunc = getattr(module, function), func - globals()['__old_' + func.__name__] = oldfunc - except Exception: - if warn: - warnings.warn("failed to import %s" % module_function) - return func - - return decorate - - -def decodejpg(encoded, tables=b'', photometric=None, - ycbcr_subsampling=None, ycbcr_positioning=None): - """Decode JPEG encoded byte string (using _czifile extension module).""" - import _czifile - image = _czifile.decodejpg(encoded, tables) - if photometric == 'rgb' and ycbcr_subsampling and ycbcr_positioning: - # TODO: convert YCbCr to RGB - pass - return image.tostring() - - -@_replace_by('_tifffile.decodepackbits') -def decodepackbits(encoded): - """Decompress PackBits encoded byte string. - - PackBits is a simple byte-oriented run-length compression scheme. - - """ - func = ord if sys.version[0] == '2' else lambda x: x - result = [] - result_extend = result.extend - i = 0 - try: - while True: - n = func(encoded[i]) + 1 - i += 1 - if n < 129: - result_extend(encoded[i:i+n]) - i += n - elif n > 129: - result_extend(encoded[i:i+1] * (258-n)) - i += 1 - except IndexError: - pass - return b''.join(result) if sys.version[0] == '2' else bytes(result) - - -@_replace_by('_tifffile.decodelzw') -def decodelzw(encoded): - """Decompress LZW (Lempel-Ziv-Welch) encoded TIFF strip (byte string). - - The strip must begin with a CLEAR code and end with an EOI code. - - This is an implementation of the LZW decoding algorithm described in (1). - It is not compatible with old style LZW compressed files like quad-lzw.tif. - - """ - len_encoded = len(encoded) - bitcount_max = len_encoded * 8 - unpack = struct.unpack - - if sys.version[0] == '2': - newtable = [chr(i) for i in range(256)] - else: - newtable = [bytes([i]) for i in range(256)] - newtable.extend((0, 0)) - - def next_code(): - """Return integer of `bitw` bits at `bitcount` position in encoded.""" - start = bitcount // 8 - s = encoded[start:start+4] - try: - code = unpack('>I', s)[0] - except Exception: - code = unpack('>I', s + b'\x00'*(4-len(s)))[0] - code <<= bitcount % 8 - code &= mask - return code >> shr - - switchbitch = { # code: bit-width, shr-bits, bit-mask - 255: (9, 23, int(9*'1'+'0'*23, 2)), - 511: (10, 22, int(10*'1'+'0'*22, 2)), - 1023: (11, 21, int(11*'1'+'0'*21, 2)), - 2047: (12, 20, int(12*'1'+'0'*20, 2)), } - bitw, shr, mask = switchbitch[255] - bitcount = 0 - - if len_encoded < 4: - raise ValueError("strip must be at least 4 characters long") - - if next_code() != 256: - raise ValueError("strip must begin with CLEAR code") - - code = 0 - oldcode = 0 - result = [] - result_append = result.append - while True: - code = next_code() # ~5% faster when inlining this function - bitcount += bitw - if code == 257 or bitcount >= bitcount_max: # EOI - break - if code == 256: # CLEAR - table = newtable[:] - table_append = table.append - lentable = 258 - bitw, shr, mask = switchbitch[255] - code = next_code() - bitcount += bitw - if code == 257: # EOI - break - result_append(table[code]) - else: - if code < lentable: - decoded = table[code] - newcode = table[oldcode] + decoded[:1] - else: - newcode = table[oldcode] - newcode += newcode[:1] - decoded = newcode - result_append(decoded) - table_append(newcode) - lentable += 1 - oldcode = code - if lentable in switchbitch: - bitw, shr, mask = switchbitch[lentable] - - if code != 257: - warnings.warn("unexpected end of lzw stream (code %i)" % code) - - return b''.join(result) - - -@_replace_by('_tifffile.unpackints') -def unpackints(data, dtype, itemsize, runlen=0): - """Decompress byte string to array of integers of any bit size <= 32. - - Parameters - ---------- - data : byte str - Data to decompress. - dtype : numpy.dtype or str - A numpy boolean or integer type. - itemsize : int - Number of bits per integer. - runlen : int - Number of consecutive integers, after which to start at next byte. - - """ - if itemsize == 1: # bitarray - data = numpy.fromstring(data, '|B') - data = numpy.unpackbits(data) - if runlen % 8: - data = data.reshape(-1, runlen + (8 - runlen % 8)) - data = data[:, :runlen].reshape(-1) - return data.astype(dtype) - - dtype = numpy.dtype(dtype) - if itemsize in (8, 16, 32, 64): - return numpy.fromstring(data, dtype) - if itemsize < 1 or itemsize > 32: - raise ValueError("itemsize out of range: %i" % itemsize) - if dtype.kind not in "biu": - raise ValueError("invalid dtype") - - itembytes = next(i for i in (1, 2, 4, 8) if 8 * i >= itemsize) - if itembytes != dtype.itemsize: - raise ValueError("dtype.itemsize too small") - if runlen == 0: - runlen = len(data) // itembytes - skipbits = runlen*itemsize % 8 - if skipbits: - skipbits = 8 - skipbits - shrbits = itembytes*8 - itemsize - bitmask = int(itemsize*'1'+'0'*shrbits, 2) - dtypestr = '>' + dtype.char # dtype always big endian? - - unpack = struct.unpack - l = runlen * (len(data)*8 // (runlen*itemsize + skipbits)) - result = numpy.empty((l, ), dtype) - bitcount = 0 - for i in range(len(result)): - start = bitcount // 8 - s = data[start:start+itembytes] - try: - code = unpack(dtypestr, s)[0] - except Exception: - code = unpack(dtypestr, s + b'\x00'*(itembytes-len(s)))[0] - code <<= bitcount % 8 - code &= bitmask - result[i] = code >> shrbits - bitcount += itemsize - if (i+1) % runlen == 0: - bitcount += skipbits - return result - - -def unpackrgb(data, dtype='>> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff) - >>> print(unpackrgb(data, '>> print(unpackrgb(data, '>> print(unpackrgb(data, '= bits) - data = numpy.fromstring(data, dtype.byteorder+dt) - result = numpy.empty((data.size, len(bitspersample)), dtype.char) - for i, bps in enumerate(bitspersample): - t = data >> int(numpy.sum(bitspersample[i+1:])) - t &= int('0b'+'1'*bps, 2) - if rescale: - o = ((dtype.itemsize * 8) // bps + 1) * bps - if o > data.dtype.itemsize * 8: - t = t.astype('I') - t *= (2**o - 1) // (2**bps - 1) - t //= 2**(o - (dtype.itemsize * 8)) - result[:, i] = t - return result.reshape(-1) - - -def reorient(image, orientation): - """Return reoriented view of image array. - - Parameters - ---------- - image : numpy array - Non-squeezed output of asarray() functions. - Axes -3 and -2 must be image length and width respectively. - orientation : int or str - One of TIFF_ORIENTATIONS keys or values. - - """ - o = TIFF_ORIENTATIONS.get(orientation, orientation) - if o == 'top_left': - return image - elif o == 'top_right': - return image[..., ::-1, :] - elif o == 'bottom_left': - return image[..., ::-1, :, :] - elif o == 'bottom_right': - return image[..., ::-1, ::-1, :] - elif o == 'left_top': - return numpy.swapaxes(image, -3, -2) - elif o == 'right_top': - return numpy.swapaxes(image, -3, -2)[..., ::-1, :] - elif o == 'left_bottom': - return numpy.swapaxes(image, -3, -2)[..., ::-1, :, :] - elif o == 'right_bottom': - return numpy.swapaxes(image, -3, -2)[..., ::-1, ::-1, :] - - -def squeeze_axes(shape, axes, skip='XY'): - """Return shape and axes with single-dimensional entries removed. - - Remove unused dimensions unless their axes are listed in 'skip'. - - >>> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC') - ((5, 2, 1), 'TYX') - - """ - if len(shape) != len(axes): - raise ValueError("dimensions of axes and shape don't match") - shape, axes = zip(*(i for i in zip(shape, axes) - if i[0] > 1 or i[1] in skip)) - return shape, ''.join(axes) - - -def transpose_axes(data, axes, asaxes='CTZYX'): - """Return data with its axes permuted to match specified axes. - - A view is returned if possible. - - >>> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape - (5, 2, 1, 3, 4) - - """ - for ax in axes: - if ax not in asaxes: - raise ValueError("unknown axis %s" % ax) - # add missing axes to data - shape = data.shape - for ax in reversed(asaxes): - if ax not in axes: - axes = ax + axes - shape = (1,) + shape - data = data.reshape(shape) - # transpose axes - data = data.transpose([axes.index(ax) for ax in asaxes]) - return data - - -def stack_pages(pages, memmap=False, *args, **kwargs): - """Read data from sequence of TiffPage and stack them vertically. - - If memmap is True, return an array stored in a binary file on disk. - Additional parameters are passsed to the page asarray function. - - """ - if len(pages) == 0: - raise ValueError("no pages") - - if len(pages) == 1: - return pages[0].asarray(memmap=memmap, *args, **kwargs) - - result = pages[0].asarray(*args, **kwargs) - shape = (len(pages),) + result.shape - if memmap: - with tempfile.NamedTemporaryFile() as fh: - result = numpy.memmap(fh, dtype=result.dtype, shape=shape) - else: - result = numpy.empty(shape, dtype=result.dtype) - - for i, page in enumerate(pages): - result[i] = page.asarray(*args, **kwargs) - - return result - - -def stripnull(string): - """Return string truncated at first null character. - - Clean NULL terminated C strings. - - >>> stripnull(b'string\\x00') - b'string' - - """ - i = string.find(b'\x00') - return string if (i < 0) else string[:i] - - -def stripascii(string): - """Return string truncated at last byte that is 7bit ASCII. - - Clean NULL separated and terminated TIFF strings. - - >>> stripascii(b'string\\x00string\\n\\x01\\x00') - b'string\\x00string\\n' - >>> stripascii(b'\\x00') - b'' - - """ - # TODO: pythonize this - ord_ = ord if sys.version_info[0] < 3 else lambda x: x - i = len(string) - while i: - i -= 1 - if 8 < ord_(string[i]) < 127: - break - else: - i = -1 - return string[:i+1] - - -def format_size(size): - """Return file size as string from byte size.""" - for unit in ('B', 'KB', 'MB', 'GB', 'TB'): - if size < 2048: - return "%.f %s" % (size, unit) - size /= 1024.0 - - -def sequence(value): - """Return tuple containing value if value is not a sequence. - - >>> sequence(1) - (1,) - >>> sequence([1]) - [1] - - """ - try: - len(value) - return value - except TypeError: - return (value, ) - - -def product(iterable): - """Return product of sequence of numbers. - - Equivalent of functools.reduce(operator.mul, iterable, 1). - - >>> product([2**8, 2**30]) - 274877906944 - >>> product([]) - 1 - - """ - prod = 1 - for i in iterable: - prod *= i - return prod - - -def natural_sorted(iterable): - """Return human sorted list of strings. - - E.g. for sorting file names. - - >>> natural_sorted(['f1', 'f2', 'f10']) - ['f1', 'f2', 'f10'] - - """ - def sortkey(x): - return [(int(c) if c.isdigit() else c) for c in re.split(numbers, x)] - numbers = re.compile(r'(\d+)') - return sorted(iterable, key=sortkey) - - -def excel_datetime(timestamp, epoch=datetime.datetime.fromordinal(693594)): - """Return datetime object from timestamp in Excel serial format. - - Convert LSM time stamps. - - >>> excel_datetime(40237.029999999795) - datetime.datetime(2010, 2, 28, 0, 43, 11, 999982) - - """ - return epoch + datetime.timedelta(timestamp) - - -def julian_datetime(julianday, milisecond=0): - """Return datetime from days since 1/1/4713 BC and ms since midnight. - - Convert Julian dates according to MetaMorph. - - >>> julian_datetime(2451576, 54362783) - datetime.datetime(2000, 2, 2, 15, 6, 2, 783) - - """ - if julianday <= 1721423: - # no datetime before year 1 - return None - - a = julianday + 1 - if a > 2299160: - alpha = math.trunc((a - 1867216.25) / 36524.25) - a += 1 + alpha - alpha // 4 - b = a + (1524 if a > 1721423 else 1158) - c = math.trunc((b - 122.1) / 365.25) - d = math.trunc(365.25 * c) - e = math.trunc((b - d) / 30.6001) - - day = b - d - math.trunc(30.6001 * e) - month = e - (1 if e < 13.5 else 13) - year = c - (4716 if month > 2.5 else 4715) - - hour, milisecond = divmod(milisecond, 1000 * 60 * 60) - minute, milisecond = divmod(milisecond, 1000 * 60) - second, milisecond = divmod(milisecond, 1000) - - return datetime.datetime(year, month, day, - hour, minute, second, milisecond) - - -def test_tifffile(directory='testimages', verbose=True): - """Read all images in directory. - - Print error message on failure. - - >>> test_tifffile(verbose=False) - - """ - successful = 0 - failed = 0 - start = time.time() - for f in glob.glob(os.path.join(directory, '*.*')): - if verbose: - print("\n%s>\n" % f.lower(), end='') - t0 = time.time() - try: - tif = TiffFile(f, multifile=True) - except Exception as e: - if not verbose: - print(f, end=' ') - print("ERROR:", e) - failed += 1 - continue - try: - img = tif.asarray() - except ValueError: - try: - img = tif[0].asarray() - except Exception as e: - if not verbose: - print(f, end=' ') - print("ERROR:", e) - failed += 1 - continue - finally: - tif.close() - successful += 1 - if verbose: - print("%s, %s %s, %s, %.0f ms" % ( - str(tif), str(img.shape), img.dtype, tif[0].compression, - (time.time()-t0) * 1e3)) - if verbose: - print("\nSuccessfully read %i of %i files in %.3f s\n" % ( - successful, successful+failed, time.time()-start)) - - -class TIFF_SUBFILE_TYPES(object): - def __getitem__(self, key): - result = [] - if key & 1: - result.append('reduced_image') - if key & 2: - result.append('page') - if key & 4: - result.append('mask') - return tuple(result) - - -TIFF_PHOTOMETRICS = { - 0: 'miniswhite', - 1: 'minisblack', - 2: 'rgb', - 3: 'palette', - 4: 'mask', - 5: 'separated', # CMYK - 6: 'ycbcr', - 8: 'cielab', - 9: 'icclab', - 10: 'itulab', - 32803: 'cfa', # Color Filter Array - 32844: 'logl', - 32845: 'logluv', - 34892: 'linear_raw' -} - -TIFF_COMPESSIONS = { - 1: None, - 2: 'ccittrle', - 3: 'ccittfax3', - 4: 'ccittfax4', - 5: 'lzw', - 6: 'ojpeg', - 7: 'jpeg', - 8: 'adobe_deflate', - 9: 't85', - 10: 't43', - 32766: 'next', - 32771: 'ccittrlew', - 32773: 'packbits', - 32809: 'thunderscan', - 32895: 'it8ctpad', - 32896: 'it8lw', - 32897: 'it8mp', - 32898: 'it8bl', - 32908: 'pixarfilm', - 32909: 'pixarlog', - 32946: 'deflate', - 32947: 'dcs', - 34661: 'jbig', - 34676: 'sgilog', - 34677: 'sgilog24', - 34712: 'jp2000', - 34713: 'nef', -} - -TIFF_DECOMPESSORS = { - None: lambda x: x, - 'adobe_deflate': zlib.decompress, - 'deflate': zlib.decompress, - 'packbits': decodepackbits, - 'lzw': decodelzw, - # 'jpeg': decodejpg -} - -TIFF_DATA_TYPES = { - 1: '1B', # BYTE 8-bit unsigned integer. - 2: '1s', # ASCII 8-bit byte that contains a 7-bit ASCII code; - # the last byte must be NULL (binary zero). - 3: '1H', # SHORT 16-bit (2-byte) unsigned integer - 4: '1I', # LONG 32-bit (4-byte) unsigned integer. - 5: '2I', # RATIONAL Two LONGs: the first represents the numerator of - # a fraction; the second, the denominator. - 6: '1b', # SBYTE An 8-bit signed (twos-complement) integer. - 7: '1s', # UNDEFINED An 8-bit byte that may contain anything, - # depending on the definition of the field. - 8: '1h', # SSHORT A 16-bit (2-byte) signed (twos-complement) integer. - 9: '1i', # SLONG A 32-bit (4-byte) signed (twos-complement) integer. - 10: '2i', # SRATIONAL Two SLONGs: the first represents the numerator - # of a fraction, the second the denominator. - 11: '1f', # FLOAT Single precision (4-byte) IEEE format. - 12: '1d', # DOUBLE Double precision (8-byte) IEEE format. - 13: '1I', # IFD unsigned 4 byte IFD offset. - #14: '', # UNICODE - #15: '', # COMPLEX - 16: '1Q', # LONG8 unsigned 8 byte integer (BigTiff) - 17: '1q', # SLONG8 signed 8 byte integer (BigTiff) - 18: '1Q', # IFD8 unsigned 8 byte IFD offset (BigTiff) -} - -TIFF_SAMPLE_FORMATS = { - 1: 'uint', - 2: 'int', - 3: 'float', - #4: 'void', - #5: 'complex_int', - 6: 'complex', -} - -TIFF_SAMPLE_DTYPES = { - ('uint', 1): '?', # bitmap - ('uint', 2): 'B', - ('uint', 3): 'B', - ('uint', 4): 'B', - ('uint', 5): 'B', - ('uint', 6): 'B', - ('uint', 7): 'B', - ('uint', 8): 'B', - ('uint', 9): 'H', - ('uint', 10): 'H', - ('uint', 11): 'H', - ('uint', 12): 'H', - ('uint', 13): 'H', - ('uint', 14): 'H', - ('uint', 15): 'H', - ('uint', 16): 'H', - ('uint', 17): 'I', - ('uint', 18): 'I', - ('uint', 19): 'I', - ('uint', 20): 'I', - ('uint', 21): 'I', - ('uint', 22): 'I', - ('uint', 23): 'I', - ('uint', 24): 'I', - ('uint', 25): 'I', - ('uint', 26): 'I', - ('uint', 27): 'I', - ('uint', 28): 'I', - ('uint', 29): 'I', - ('uint', 30): 'I', - ('uint', 31): 'I', - ('uint', 32): 'I', - ('uint', 64): 'Q', - ('int', 8): 'b', - ('int', 16): 'h', - ('int', 32): 'i', - ('int', 64): 'q', - ('float', 16): 'e', - ('float', 32): 'f', - ('float', 64): 'd', - ('complex', 64): 'F', - ('complex', 128): 'D', - ('uint', (5, 6, 5)): 'B', -} - -TIFF_ORIENTATIONS = { - 1: 'top_left', - 2: 'top_right', - 3: 'bottom_right', - 4: 'bottom_left', - 5: 'left_top', - 6: 'right_top', - 7: 'right_bottom', - 8: 'left_bottom', -} - -# TODO: is there a standard for character axes labels? -AXES_LABELS = { - 'X': 'width', - 'Y': 'height', - 'Z': 'depth', - 'S': 'sample', # rgb(a) - 'I': 'series', # general sequence, plane, page, IFD - 'T': 'time', - 'C': 'channel', # color, emission wavelength - 'A': 'angle', - 'P': 'phase', # formerly F # P is Position in LSM! - 'R': 'tile', # region, point, mosaic - 'H': 'lifetime', # histogram - 'E': 'lambda', # excitation wavelength - 'L': 'exposure', # lux - 'V': 'event', - 'Q': 'other', - #'M': 'mosaic', # LSM 6 -} - -AXES_LABELS.update(dict((v, k) for k, v in AXES_LABELS.items())) - -# Map OME pixel types to numpy dtype -OME_PIXEL_TYPES = { - 'int8': 'i1', - 'int16': 'i2', - 'int32': 'i4', - 'uint8': 'u1', - 'uint16': 'u2', - 'uint32': 'u4', - 'float': 'f4', - # 'bit': 'bit', - 'double': 'f8', - 'complex': 'c8', - 'double-complex': 'c16', -} - -# NIH Image PicHeader v1.63 -NIH_IMAGE_HEADER = [ - ('fileid', 'a8'), - ('nlines', 'i2'), - ('pixelsperline', 'i2'), - ('version', 'i2'), - ('oldlutmode', 'i2'), - ('oldncolors', 'i2'), - ('colors', 'u1', (3, 32)), - ('oldcolorstart', 'i2'), - ('colorwidth', 'i2'), - ('extracolors', 'u2', (6, 3)), - ('nextracolors', 'i2'), - ('foregroundindex', 'i2'), - ('backgroundindex', 'i2'), - ('xscale', 'f8'), - ('_x0', 'i2'), - ('_x1', 'i2'), - ('units_t', 'i2'), # NIH_UNITS_TYPE - ('p1', [('x', 'i2'), ('y', 'i2')]), - ('p2', [('x', 'i2'), ('y', 'i2')]), - ('curvefit_t', 'i2'), # NIH_CURVEFIT_TYPE - ('ncoefficients', 'i2'), - ('coeff', 'f8', 6), - ('_um_len', 'u1'), - ('um', 'a15'), - ('_x2', 'u1'), - ('binarypic', 'b1'), - ('slicestart', 'i2'), - ('sliceend', 'i2'), - ('scalemagnification', 'f4'), - ('nslices', 'i2'), - ('slicespacing', 'f4'), - ('currentslice', 'i2'), - ('frameinterval', 'f4'), - ('pixelaspectratio', 'f4'), - ('colorstart', 'i2'), - ('colorend', 'i2'), - ('ncolors', 'i2'), - ('fill1', '3u2'), - ('fill2', '3u2'), - ('colortable_t', 'u1'), # NIH_COLORTABLE_TYPE - ('lutmode_t', 'u1'), # NIH_LUTMODE_TYPE - ('invertedtable', 'b1'), - ('zeroclip', 'b1'), - ('_xunit_len', 'u1'), - ('xunit', 'a11'), - ('stacktype_t', 'i2'), # NIH_STACKTYPE_TYPE -] - -NIH_COLORTABLE_TYPE = ( - 'CustomTable', 'AppleDefault', 'Pseudo20', 'Pseudo32', 'Rainbow', - 'Fire1', 'Fire2', 'Ice', 'Grays', 'Spectrum') - -NIH_LUTMODE_TYPE = ( - 'PseudoColor', 'OldAppleDefault', 'OldSpectrum', 'GrayScale', - 'ColorLut', 'CustomGrayscale') - -NIH_CURVEFIT_TYPE = ( - 'StraightLine', 'Poly2', 'Poly3', 'Poly4', 'Poly5', 'ExpoFit', - 'PowerFit', 'LogFit', 'RodbardFit', 'SpareFit1', 'Uncalibrated', - 'UncalibratedOD') - -NIH_UNITS_TYPE = ( - 'Nanometers', 'Micrometers', 'Millimeters', 'Centimeters', 'Meters', - 'Kilometers', 'Inches', 'Feet', 'Miles', 'Pixels', 'OtherUnits') - -NIH_STACKTYPE_TYPE = ( - 'VolumeStack', 'RGBStack', 'MovieStack', 'HSVStack') - -# Map Universal Imaging Corporation MetaMorph internal tag ids to name and type -UIC_TAGS = { - 0: ('auto_scale', int), - 1: ('min_scale', int), - 2: ('max_scale', int), - 3: ('spatial_calibration', int), - 4: ('x_calibration', Fraction), - 5: ('y_calibration', Fraction), - 6: ('calibration_units', str), - 7: ('name', str), - 8: ('thresh_state', int), - 9: ('thresh_state_red', int), - 10: ('tagid_10', None), # undefined - 11: ('thresh_state_green', int), - 12: ('thresh_state_blue', int), - 13: ('thresh_state_lo', int), - 14: ('thresh_state_hi', int), - 15: ('zoom', int), - 16: ('create_time', julian_datetime), - 17: ('last_saved_time', julian_datetime), - 18: ('current_buffer', int), - 19: ('gray_fit', None), - 20: ('gray_point_count', None), - 21: ('gray_x', Fraction), - 22: ('gray_y', Fraction), - 23: ('gray_min', Fraction), - 24: ('gray_max', Fraction), - 25: ('gray_unit_name', str), - 26: ('standard_lut', int), - 27: ('wavelength', int), - 28: ('stage_position', '(%i,2,2)u4'), # N xy positions as fractions - 29: ('camera_chip_offset', '(%i,2,2)u4'), # N xy offsets as fractions - 30: ('overlay_mask', None), - 31: ('overlay_compress', None), - 32: ('overlay', None), - 33: ('special_overlay_mask', None), - 34: ('special_overlay_compress', None), - 35: ('special_overlay', None), - 36: ('image_property', read_uic_image_property), - 37: ('stage_label', '%ip'), # N str - 38: ('autoscale_lo_info', Fraction), - 39: ('autoscale_hi_info', Fraction), - 40: ('absolute_z', '(%i,2)u4'), # N fractions - 41: ('absolute_z_valid', '(%i,)u4'), # N long - 42: ('gamma', int), - 43: ('gamma_red', int), - 44: ('gamma_green', int), - 45: ('gamma_blue', int), - 46: ('camera_bin', int), - 47: ('new_lut', int), - 48: ('image_property_ex', None), - 49: ('plane_property', int), - 50: ('user_lut_table', '(256,3)u1'), - 51: ('red_autoscale_info', int), - 52: ('red_autoscale_lo_info', Fraction), - 53: ('red_autoscale_hi_info', Fraction), - 54: ('red_minscale_info', int), - 55: ('red_maxscale_info', int), - 56: ('green_autoscale_info', int), - 57: ('green_autoscale_lo_info', Fraction), - 58: ('green_autoscale_hi_info', Fraction), - 59: ('green_minscale_info', int), - 60: ('green_maxscale_info', int), - 61: ('blue_autoscale_info', int), - 62: ('blue_autoscale_lo_info', Fraction), - 63: ('blue_autoscale_hi_info', Fraction), - 64: ('blue_min_scale_info', int), - 65: ('blue_max_scale_info', int), - #66: ('overlay_plane_color', read_uic_overlay_plane_color), -} - - -# Olympus FluoView -MM_DIMENSION = [ - ('name', 'a16'), - ('size', 'i4'), - ('origin', 'f8'), - ('resolution', 'f8'), - ('unit', 'a64'), -] - -MM_HEADER = [ - ('header_flag', 'i2'), - ('image_type', 'u1'), - ('image_name', 'a257'), - ('offset_data', 'u4'), - ('palette_size', 'i4'), - ('offset_palette0', 'u4'), - ('offset_palette1', 'u4'), - ('comment_size', 'i4'), - ('offset_comment', 'u4'), - ('dimensions', MM_DIMENSION, 10), - ('offset_position', 'u4'), - ('map_type', 'i2'), - ('map_min', 'f8'), - ('map_max', 'f8'), - ('min_value', 'f8'), - ('max_value', 'f8'), - ('offset_map', 'u4'), - ('gamma', 'f8'), - ('offset', 'f8'), - ('gray_channel', MM_DIMENSION), - ('offset_thumbnail', 'u4'), - ('voice_field', 'i4'), - ('offset_voice_field', 'u4'), -] - -# Carl Zeiss LSM -CZ_LSM_INFO = [ - ('magic_number', 'u4'), - ('structure_size', 'i4'), - ('dimension_x', 'i4'), - ('dimension_y', 'i4'), - ('dimension_z', 'i4'), - ('dimension_channels', 'i4'), - ('dimension_time', 'i4'), - ('data_type', 'i4'), # CZ_DATA_TYPES - ('thumbnail_x', 'i4'), - ('thumbnail_y', 'i4'), - ('voxel_size_x', 'f8'), - ('voxel_size_y', 'f8'), - ('voxel_size_z', 'f8'), - ('origin_x', 'f8'), - ('origin_y', 'f8'), - ('origin_z', 'f8'), - ('scan_type', 'u2'), - ('spectral_scan', 'u2'), - ('type_of_data', 'u4'), # CZ_TYPE_OF_DATA - ('offset_vector_overlay', 'u4'), - ('offset_input_lut', 'u4'), - ('offset_output_lut', 'u4'), - ('offset_channel_colors', 'u4'), - ('time_interval', 'f8'), - ('offset_channel_data_types', 'u4'), - ('offset_scan_info', 'u4'), # CZ_LSM_SCAN_INFO - ('offset_ks_data', 'u4'), - ('offset_time_stamps', 'u4'), - ('offset_event_list', 'u4'), - ('offset_roi', 'u4'), - ('offset_bleach_roi', 'u4'), - ('offset_next_recording', 'u4'), - # LSM 2.0 ends here - ('display_aspect_x', 'f8'), - ('display_aspect_y', 'f8'), - ('display_aspect_z', 'f8'), - ('display_aspect_time', 'f8'), - ('offset_mean_of_roi_overlay', 'u4'), - ('offset_topo_isoline_overlay', 'u4'), - ('offset_topo_profile_overlay', 'u4'), - ('offset_linescan_overlay', 'u4'), - ('offset_toolbar_flags', 'u4'), - ('offset_channel_wavelength', 'u4'), - ('offset_channel_factors', 'u4'), - ('objective_sphere_correction', 'f8'), - ('offset_unmix_parameters', 'u4'), - # LSM 3.2, 4.0 end here - ('offset_acquisition_parameters', 'u4'), - ('offset_characteristics', 'u4'), - ('offset_palette', 'u4'), - ('time_difference_x', 'f8'), - ('time_difference_y', 'f8'), - ('time_difference_z', 'f8'), - ('internal_use_1', 'u4'), - ('dimension_p', 'i4'), - ('dimension_m', 'i4'), - ('dimensions_reserved', '16i4'), - ('offset_tile_positions', 'u4'), - ('reserved_1', '9u4'), - ('offset_positions', 'u4'), - ('reserved_2', '21u4'), # must be 0 -] - -# Import functions for LSM_INFO sub-records -CZ_LSM_INFO_READERS = { - 'scan_info': read_cz_lsm_scan_info, - 'time_stamps': read_cz_lsm_time_stamps, - 'event_list': read_cz_lsm_event_list, - 'channel_colors': read_cz_lsm_floatpairs, - 'positions': read_cz_lsm_floatpairs, - 'tile_positions': read_cz_lsm_floatpairs, -} - -# Map cz_lsm_info.scan_type to dimension order -CZ_SCAN_TYPES = { - 0: 'XYZCT', # x-y-z scan - 1: 'XYZCT', # z scan (x-z plane) - 2: 'XYZCT', # line scan - 3: 'XYTCZ', # time series x-y - 4: 'XYZTC', # time series x-z - 5: 'XYTCZ', # time series 'Mean of ROIs' - 6: 'XYZTC', # time series x-y-z - 7: 'XYCTZ', # spline scan - 8: 'XYCZT', # spline scan x-z - 9: 'XYTCZ', # time series spline plane x-z - 10: 'XYZCT', # point mode -} - -# Map dimension codes to cz_lsm_info attribute -CZ_DIMENSIONS = { - 'X': 'dimension_x', - 'Y': 'dimension_y', - 'Z': 'dimension_z', - 'C': 'dimension_channels', - 'T': 'dimension_time', -} - -# Description of cz_lsm_info.data_type -CZ_DATA_TYPES = { - 0: 'varying data types', - 1: '8 bit unsigned integer', - 2: '12 bit unsigned integer', - 5: '32 bit float', -} - -# Description of cz_lsm_info.type_of_data -CZ_TYPE_OF_DATA = { - 0: 'Original scan data', - 1: 'Calculated data', - 2: '3D reconstruction', - 3: 'Topography height map', -} - -CZ_LSM_SCAN_INFO_ARRAYS = { - 0x20000000: "tracks", - 0x30000000: "lasers", - 0x60000000: "detection_channels", - 0x80000000: "illumination_channels", - 0xa0000000: "beam_splitters", - 0xc0000000: "data_channels", - 0x11000000: "timers", - 0x13000000: "markers", -} - -CZ_LSM_SCAN_INFO_STRUCTS = { - # 0x10000000: "recording", - 0x40000000: "track", - 0x50000000: "laser", - 0x70000000: "detection_channel", - 0x90000000: "illumination_channel", - 0xb0000000: "beam_splitter", - 0xd0000000: "data_channel", - 0x12000000: "timer", - 0x14000000: "marker", -} - -CZ_LSM_SCAN_INFO_ATTRIBUTES = { - # recording - 0x10000001: "name", - 0x10000002: "description", - 0x10000003: "notes", - 0x10000004: "objective", - 0x10000005: "processing_summary", - 0x10000006: "special_scan_mode", - 0x10000007: "scan_type", - 0x10000008: "scan_mode", - 0x10000009: "number_of_stacks", - 0x1000000a: "lines_per_plane", - 0x1000000b: "samples_per_line", - 0x1000000c: "planes_per_volume", - 0x1000000d: "images_width", - 0x1000000e: "images_height", - 0x1000000f: "images_number_planes", - 0x10000010: "images_number_stacks", - 0x10000011: "images_number_channels", - 0x10000012: "linscan_xy_size", - 0x10000013: "scan_direction", - 0x10000014: "time_series", - 0x10000015: "original_scan_data", - 0x10000016: "zoom_x", - 0x10000017: "zoom_y", - 0x10000018: "zoom_z", - 0x10000019: "sample_0x", - 0x1000001a: "sample_0y", - 0x1000001b: "sample_0z", - 0x1000001c: "sample_spacing", - 0x1000001d: "line_spacing", - 0x1000001e: "plane_spacing", - 0x1000001f: "plane_width", - 0x10000020: "plane_height", - 0x10000021: "volume_depth", - 0x10000023: "nutation", - 0x10000034: "rotation", - 0x10000035: "precession", - 0x10000036: "sample_0time", - 0x10000037: "start_scan_trigger_in", - 0x10000038: "start_scan_trigger_out", - 0x10000039: "start_scan_event", - 0x10000040: "start_scan_time", - 0x10000041: "stop_scan_trigger_in", - 0x10000042: "stop_scan_trigger_out", - 0x10000043: "stop_scan_event", - 0x10000044: "stop_scan_time", - 0x10000045: "use_rois", - 0x10000046: "use_reduced_memory_rois", - 0x10000047: "user", - 0x10000048: "use_bc_correction", - 0x10000049: "position_bc_correction1", - 0x10000050: "position_bc_correction2", - 0x10000051: "interpolation_y", - 0x10000052: "camera_binning", - 0x10000053: "camera_supersampling", - 0x10000054: "camera_frame_width", - 0x10000055: "camera_frame_height", - 0x10000056: "camera_offset_x", - 0x10000057: "camera_offset_y", - 0x10000059: "rt_binning", - 0x1000005a: "rt_frame_width", - 0x1000005b: "rt_frame_height", - 0x1000005c: "rt_region_width", - 0x1000005d: "rt_region_height", - 0x1000005e: "rt_offset_x", - 0x1000005f: "rt_offset_y", - 0x10000060: "rt_zoom", - 0x10000061: "rt_line_period", - 0x10000062: "prescan", - 0x10000063: "scan_direction_z", - # track - 0x40000001: "multiplex_type", # 0 after line; 1 after frame - 0x40000002: "multiplex_order", - 0x40000003: "sampling_mode", # 0 sample; 1 line average; 2 frame average - 0x40000004: "sampling_method", # 1 mean; 2 sum - 0x40000005: "sampling_number", - 0x40000006: "acquire", - 0x40000007: "sample_observation_time", - 0x4000000b: "time_between_stacks", - 0x4000000c: "name", - 0x4000000d: "collimator1_name", - 0x4000000e: "collimator1_position", - 0x4000000f: "collimator2_name", - 0x40000010: "collimator2_position", - 0x40000011: "is_bleach_track", - 0x40000012: "is_bleach_after_scan_number", - 0x40000013: "bleach_scan_number", - 0x40000014: "trigger_in", - 0x40000015: "trigger_out", - 0x40000016: "is_ratio_track", - 0x40000017: "bleach_count", - 0x40000018: "spi_center_wavelength", - 0x40000019: "pixel_time", - 0x40000021: "condensor_frontlens", - 0x40000023: "field_stop_value", - 0x40000024: "id_condensor_aperture", - 0x40000025: "condensor_aperture", - 0x40000026: "id_condensor_revolver", - 0x40000027: "condensor_filter", - 0x40000028: "id_transmission_filter1", - 0x40000029: "id_transmission1", - 0x40000030: "id_transmission_filter2", - 0x40000031: "id_transmission2", - 0x40000032: "repeat_bleach", - 0x40000033: "enable_spot_bleach_pos", - 0x40000034: "spot_bleach_posx", - 0x40000035: "spot_bleach_posy", - 0x40000036: "spot_bleach_posz", - 0x40000037: "id_tubelens", - 0x40000038: "id_tubelens_position", - 0x40000039: "transmitted_light", - 0x4000003a: "reflected_light", - 0x4000003b: "simultan_grab_and_bleach", - 0x4000003c: "bleach_pixel_time", - # laser - 0x50000001: "name", - 0x50000002: "acquire", - 0x50000003: "power", - # detection_channel - 0x70000001: "integration_mode", - 0x70000002: "special_mode", - 0x70000003: "detector_gain_first", - 0x70000004: "detector_gain_last", - 0x70000005: "amplifier_gain_first", - 0x70000006: "amplifier_gain_last", - 0x70000007: "amplifier_offs_first", - 0x70000008: "amplifier_offs_last", - 0x70000009: "pinhole_diameter", - 0x7000000a: "counting_trigger", - 0x7000000b: "acquire", - 0x7000000c: "point_detector_name", - 0x7000000d: "amplifier_name", - 0x7000000e: "pinhole_name", - 0x7000000f: "filter_set_name", - 0x70000010: "filter_name", - 0x70000013: "integrator_name", - 0x70000014: "channel_name", - 0x70000015: "detector_gain_bc1", - 0x70000016: "detector_gain_bc2", - 0x70000017: "amplifier_gain_bc1", - 0x70000018: "amplifier_gain_bc2", - 0x70000019: "amplifier_offset_bc1", - 0x70000020: "amplifier_offset_bc2", - 0x70000021: "spectral_scan_channels", - 0x70000022: "spi_wavelength_start", - 0x70000023: "spi_wavelength_stop", - 0x70000026: "dye_name", - 0x70000027: "dye_folder", - # illumination_channel - 0x90000001: "name", - 0x90000002: "power", - 0x90000003: "wavelength", - 0x90000004: "aquire", - 0x90000005: "detchannel_name", - 0x90000006: "power_bc1", - 0x90000007: "power_bc2", - # beam_splitter - 0xb0000001: "filter_set", - 0xb0000002: "filter", - 0xb0000003: "name", - # data_channel - 0xd0000001: "name", - 0xd0000003: "acquire", - 0xd0000004: "color", - 0xd0000005: "sample_type", - 0xd0000006: "bits_per_sample", - 0xd0000007: "ratio_type", - 0xd0000008: "ratio_track1", - 0xd0000009: "ratio_track2", - 0xd000000a: "ratio_channel1", - 0xd000000b: "ratio_channel2", - 0xd000000c: "ratio_const1", - 0xd000000d: "ratio_const2", - 0xd000000e: "ratio_const3", - 0xd000000f: "ratio_const4", - 0xd0000010: "ratio_const5", - 0xd0000011: "ratio_const6", - 0xd0000012: "ratio_first_images1", - 0xd0000013: "ratio_first_images2", - 0xd0000014: "dye_name", - 0xd0000015: "dye_folder", - 0xd0000016: "spectrum", - 0xd0000017: "acquire", - # timer - 0x12000001: "name", - 0x12000002: "description", - 0x12000003: "interval", - 0x12000004: "trigger_in", - 0x12000005: "trigger_out", - 0x12000006: "activation_time", - 0x12000007: "activation_number", - # marker - 0x14000001: "name", - 0x14000002: "description", - 0x14000003: "trigger_in", - 0x14000004: "trigger_out", -} - -# Map TIFF tag code to attribute name, default value, type, count, validator -TIFF_TAGS = { - 254: ('new_subfile_type', 0, 4, 1, TIFF_SUBFILE_TYPES()), - 255: ('subfile_type', None, 3, 1, - {0: 'undefined', 1: 'image', 2: 'reduced_image', 3: 'page'}), - 256: ('image_width', None, 4, 1, None), - 257: ('image_length', None, 4, 1, None), - 258: ('bits_per_sample', 1, 3, 1, None), - 259: ('compression', 1, 3, 1, TIFF_COMPESSIONS), - 262: ('photometric', None, 3, 1, TIFF_PHOTOMETRICS), - 266: ('fill_order', 1, 3, 1, {1: 'msb2lsb', 2: 'lsb2msb'}), - 269: ('document_name', None, 2, None, None), - 270: ('image_description', None, 2, None, None), - 271: ('make', None, 2, None, None), - 272: ('model', None, 2, None, None), - 273: ('strip_offsets', None, 4, None, None), - 274: ('orientation', 1, 3, 1, TIFF_ORIENTATIONS), - 277: ('samples_per_pixel', 1, 3, 1, None), - 278: ('rows_per_strip', 2**32-1, 4, 1, None), - 279: ('strip_byte_counts', None, 4, None, None), - 280: ('min_sample_value', None, 3, None, None), - 281: ('max_sample_value', None, 3, None, None), # 2**bits_per_sample - 282: ('x_resolution', None, 5, 1, None), - 283: ('y_resolution', None, 5, 1, None), - 284: ('planar_configuration', 1, 3, 1, {1: 'contig', 2: 'separate'}), - 285: ('page_name', None, 2, None, None), - 286: ('x_position', None, 5, 1, None), - 287: ('y_position', None, 5, 1, None), - 296: ('resolution_unit', 2, 4, 1, {1: 'none', 2: 'inch', 3: 'centimeter'}), - 297: ('page_number', None, 3, 2, None), - 305: ('software', None, 2, None, None), - 306: ('datetime', None, 2, None, None), - 315: ('artist', None, 2, None, None), - 316: ('host_computer', None, 2, None, None), - 317: ('predictor', 1, 3, 1, {1: None, 2: 'horizontal'}), - 318: ('white_point', None, 5, 2, None), - 319: ('primary_chromaticities', None, 5, 6, None), - 320: ('color_map', None, 3, None, None), - 322: ('tile_width', None, 4, 1, None), - 323: ('tile_length', None, 4, 1, None), - 324: ('tile_offsets', None, 4, None, None), - 325: ('tile_byte_counts', None, 4, None, None), - 338: ('extra_samples', None, 3, None, - {0: 'unspecified', 1: 'assocalpha', 2: 'unassalpha'}), - 339: ('sample_format', 1, 3, 1, TIFF_SAMPLE_FORMATS), - 340: ('smin_sample_value', None, None, None, None), - 341: ('smax_sample_value', None, None, None, None), - 347: ('jpeg_tables', None, 7, None, None), - 530: ('ycbcr_subsampling', 1, 3, 2, None), - 531: ('ycbcr_positioning', 1, 3, 1, None), - 32996: ('sgi_matteing', None, None, 1, None), # use extra_samples - 32996: ('sgi_datatype', None, None, 1, None), # use sample_format - 32997: ('image_depth', None, 4, 1, None), - 32998: ('tile_depth', None, 4, 1, None), - 33432: ('copyright', None, 1, None, None), - 33445: ('md_file_tag', None, 4, 1, None), - 33446: ('md_scale_pixel', None, 5, 1, None), - 33447: ('md_color_table', None, 3, None, None), - 33448: ('md_lab_name', None, 2, None, None), - 33449: ('md_sample_info', None, 2, None, None), - 33450: ('md_prep_date', None, 2, None, None), - 33451: ('md_prep_time', None, 2, None, None), - 33452: ('md_file_units', None, 2, None, None), - 33550: ('model_pixel_scale', None, 12, 3, None), - 33922: ('model_tie_point', None, 12, None, None), - 34665: ('exif_ifd', None, None, 1, None), - 34735: ('geo_key_directory', None, 3, None, None), - 34736: ('geo_double_params', None, 12, None, None), - 34737: ('geo_ascii_params', None, 2, None, None), - 34853: ('gps_ifd', None, None, 1, None), - 37510: ('user_comment', None, None, None, None), - 42112: ('gdal_metadata', None, 2, None, None), - 42113: ('gdal_nodata', None, 2, None, None), - 50289: ('mc_xy_position', None, 12, 2, None), - 50290: ('mc_z_position', None, 12, 1, None), - 50291: ('mc_xy_calibration', None, 12, 3, None), - 50292: ('mc_lens_lem_na_n', None, 12, 3, None), - 50293: ('mc_channel_name', None, 1, None, None), - 50294: ('mc_ex_wavelength', None, 12, 1, None), - 50295: ('mc_time_stamp', None, 12, 1, None), - 50838: ('imagej_byte_counts', None, None, None, None), - 65200: ('flex_xml', None, 2, None, None), - # code: (attribute name, default value, type, count, validator) -} - -# Map custom TIFF tag codes to attribute names and import functions -CUSTOM_TAGS = { - 700: ('xmp', read_bytes), - 34377: ('photoshop', read_numpy), - 33723: ('iptc', read_bytes), - 34675: ('icc_profile', read_bytes), - 33628: ('uic1tag', read_uic1tag), # Universal Imaging Corporation STK - 33629: ('uic2tag', read_uic2tag), - 33630: ('uic3tag', read_uic3tag), - 33631: ('uic4tag', read_uic4tag), - 34361: ('mm_header', read_mm_header), # Olympus FluoView - 34362: ('mm_stamp', read_mm_stamp), - 34386: ('mm_user_block', read_bytes), - 34412: ('cz_lsm_info', read_cz_lsm_info), # Carl Zeiss LSM - 43314: ('nih_image_header', read_nih_image_header), - # 40001: ('mc_ipwinscal', read_bytes), - 40100: ('mc_id_old', read_bytes), - 50288: ('mc_id', read_bytes), - 50296: ('mc_frame_properties', read_bytes), - 50839: ('imagej_metadata', read_bytes), - 51123: ('micromanager_metadata', read_json), -} - -# Max line length of printed output -PRINT_LINE_LEN = 79 - - -def imshow(data, title=None, vmin=0, vmax=None, cmap=None, - bitspersample=None, photometric='rgb', interpolation='nearest', - dpi=96, figure=None, subplot=111, maxdim=8192, **kwargs): - """Plot n-dimensional images using matplotlib.pyplot. - - Return figure, subplot and plot axis. - Requires pyplot already imported ``from matplotlib import pyplot``. - - Parameters - ---------- - bitspersample : int or None - Number of bits per channel in integer RGB images. - photometric : {'miniswhite', 'minisblack', 'rgb', or 'palette'} - The color space of the image data. - title : str - Window and subplot title. - figure : matplotlib.figure.Figure (optional). - Matplotlib to use for plotting. - subplot : int - A matplotlib.pyplot.subplot axis. - maxdim : int - maximum image size in any dimension. - kwargs : optional - Arguments for matplotlib.pyplot.imshow. - - """ - #if photometric not in ('miniswhite', 'minisblack', 'rgb', 'palette'): - # raise ValueError("Can't handle %s photometrics" % photometric) - # TODO: handle photometric == 'separated' (CMYK) - isrgb = photometric in ('rgb', 'palette') - data = numpy.atleast_2d(data.squeeze()) - data = data[(slice(0, maxdim), ) * len(data.shape)] - - dims = data.ndim - if dims < 2: - raise ValueError("not an image") - elif dims == 2: - dims = 0 - isrgb = False - else: - if isrgb and data.shape[-3] in (3, 4): - data = numpy.swapaxes(data, -3, -2) - data = numpy.swapaxes(data, -2, -1) - elif not isrgb and (data.shape[-1] < data.shape[-2] // 16 and - data.shape[-1] < data.shape[-3] // 16 and - data.shape[-1] < 5): - data = numpy.swapaxes(data, -3, -1) - data = numpy.swapaxes(data, -2, -1) - isrgb = isrgb and data.shape[-1] in (3, 4) - dims -= 3 if isrgb else 2 - - if photometric == 'palette' and isrgb: - datamax = data.max() - if datamax > 255: - data >>= 8 # possible precision loss - data = data.astype('B') - elif data.dtype.kind in 'ui': - if not (isrgb and data.dtype.itemsize <= 1) or bitspersample is None: - try: - bitspersample = int(math.ceil(math.log(data.max(), 2))) - except Exception: - bitspersample = data.dtype.itemsize * 8 - elif not isinstance(bitspersample, int): - # bitspersample can be tuple, e.g. (5, 6, 5) - bitspersample = data.dtype.itemsize * 8 - datamax = 2**bitspersample - if isrgb: - if bitspersample < 8: - data <<= 8 - bitspersample - elif bitspersample > 8: - data >>= bitspersample - 8 # precision loss - data = data.astype('B') - elif data.dtype.kind == 'f': - datamax = data.max() - if isrgb and datamax > 1.0: - if data.dtype.char == 'd': - data = data.astype('f') - data /= datamax - elif data.dtype.kind == 'b': - datamax = 1 - elif data.dtype.kind == 'c': - raise NotImplementedError("complex type") # TODO: handle complex types - - if not isrgb: - if vmax is None: - vmax = datamax - if vmin is None: - if data.dtype.kind == 'i': - dtmin = numpy.iinfo(data.dtype).min - vmin = numpy.min(data) - if vmin == dtmin: - vmin = numpy.min(data > dtmin) - if data.dtype.kind == 'f': - dtmin = numpy.finfo(data.dtype).min - vmin = numpy.min(data) - if vmin == dtmin: - vmin = numpy.min(data > dtmin) - else: - vmin = 0 - - pyplot = sys.modules['matplotlib.pyplot'] - - if figure is None: - pyplot.rc('font', family='sans-serif', weight='normal', size=8) - figure = pyplot.figure(dpi=dpi, figsize=(10.3, 6.3), frameon=True, - facecolor='1.0', edgecolor='w') - try: - figure.canvas.manager.window.title(title) - except Exception: - pass - pyplot.subplots_adjust(bottom=0.03*(dims+2), top=0.9, - left=0.1, right=0.95, hspace=0.05, wspace=0.0) - subplot = pyplot.subplot(subplot) - - if title: - try: - title = unicode(title, 'Windows-1252') - except TypeError: - pass - pyplot.title(title, size=11) - - if cmap is None: - if data.dtype.kind in 'ubf' or vmin == 0: - cmap = 'cubehelix' - else: - cmap = 'coolwarm' - if photometric == 'miniswhite': - cmap += '_r' - - image = pyplot.imshow(data[(0, ) * dims].squeeze(), vmin=vmin, vmax=vmax, - cmap=cmap, interpolation=interpolation, **kwargs) - - if not isrgb: - pyplot.colorbar() # panchor=(0.55, 0.5), fraction=0.05 - - def format_coord(x, y): - # callback function to format coordinate display in toolbar - x = int(x + 0.5) - y = int(y + 0.5) - try: - if dims: - return "%s @ %s [%4i, %4i]" % (cur_ax_dat[1][y, x], - current, x, y) - else: - return "%s @ [%4i, %4i]" % (data[y, x], x, y) - except IndexError: - return "" - - pyplot.gca().format_coord = format_coord - - if dims: - current = list((0, ) * dims) - cur_ax_dat = [0, data[tuple(current)].squeeze()] - sliders = [pyplot.Slider( - pyplot.axes([0.125, 0.03*(axis+1), 0.725, 0.025]), - 'Dimension %i' % axis, 0, data.shape[axis]-1, 0, facecolor='0.5', - valfmt='%%.0f [%i]' % data.shape[axis]) for axis in range(dims)] - for slider in sliders: - slider.drawon = False - - def set_image(current, sliders=sliders, data=data): - # change image and redraw canvas - cur_ax_dat[1] = data[tuple(current)].squeeze() - image.set_data(cur_ax_dat[1]) - for ctrl, index in zip(sliders, current): - ctrl.eventson = False - ctrl.set_val(index) - ctrl.eventson = True - figure.canvas.draw() - - def on_changed(index, axis, data=data, current=current): - # callback function for slider change event - index = int(round(index)) - cur_ax_dat[0] = axis - if index == current[axis]: - return - if index >= data.shape[axis]: - index = 0 - elif index < 0: - index = data.shape[axis] - 1 - current[axis] = index - set_image(current) - - def on_keypressed(event, data=data, current=current): - # callback function for key press event - key = event.key - axis = cur_ax_dat[0] - if str(key) in '0123456789': - on_changed(key, axis) - elif key == 'right': - on_changed(current[axis] + 1, axis) - elif key == 'left': - on_changed(current[axis] - 1, axis) - elif key == 'up': - cur_ax_dat[0] = 0 if axis == len(data.shape)-1 else axis + 1 - elif key == 'down': - cur_ax_dat[0] = len(data.shape)-1 if axis == 0 else axis - 1 - elif key == 'end': - on_changed(data.shape[axis] - 1, axis) - elif key == 'home': - on_changed(0, axis) - - figure.canvas.mpl_connect('key_press_event', on_keypressed) - for axis, ctrl in enumerate(sliders): - ctrl.on_changed(lambda k, a=axis: on_changed(k, a)) - - return figure, subplot, image - - -def _app_show(): - """Block the GUI. For use as skimage plugin.""" - pyplot = sys.modules['matplotlib.pyplot'] - pyplot.show() - - -def main(argv=None): - """Command line usage main function.""" - if float(sys.version[0:3]) < 2.6: - print("This script requires Python version 2.6 or better.") - print("This is Python version %s" % sys.version) - return 0 - if argv is None: - argv = sys.argv - - import optparse - - parser = optparse.OptionParser( - usage="usage: %prog [options] path", - description="Display image data in TIFF files.", - version="%%prog %s" % __version__) - opt = parser.add_option - opt('-p', '--page', dest='page', type='int', default=-1, - help="display single page") - opt('-s', '--series', dest='series', type='int', default=-1, - help="display series of pages of same shape") - opt('--nomultifile', dest='nomultifile', action='store_true', - default=False, help="don't read OME series from multiple files") - opt('--noplot', dest='noplot', action='store_true', default=False, - help="don't display images") - opt('--interpol', dest='interpol', metavar='INTERPOL', default='bilinear', - help="image interpolation method") - opt('--dpi', dest='dpi', type='int', default=96, - help="set plot resolution") - opt('--debug', dest='debug', action='store_true', default=False, - help="raise exception on failures") - opt('--test', dest='test', action='store_true', default=False, - help="try read all images in path") - opt('--doctest', dest='doctest', action='store_true', default=False, - help="runs the docstring examples") - opt('-v', '--verbose', dest='verbose', action='store_true', default=True) - opt('-q', '--quiet', dest='verbose', action='store_false') - - settings, path = parser.parse_args() - path = ' '.join(path) - - if settings.doctest: - import doctest - doctest.testmod() - return 0 - if not path: - parser.error("No file specified") - if settings.test: - test_tifffile(path, settings.verbose) - return 0 - - if any(i in path for i in '?*'): - path = glob.glob(path) - if not path: - print('no files match the pattern') - return 0 - # TODO: handle image sequences - #if len(path) == 1: - path = path[0] - - print("Reading file structure...", end=' ') - start = time.time() - try: - tif = TiffFile(path, multifile=not settings.nomultifile) - except Exception as e: - if settings.debug: - raise - else: - print("\n", e) - sys.exit(0) - print("%.3f ms" % ((time.time()-start) * 1e3)) - - if tif.is_ome: - settings.norgb = True - - images = [(None, tif[0 if settings.page < 0 else settings.page])] - if not settings.noplot: - print("Reading image data... ", end=' ') - - def notnone(x): - return next(i for i in x if i is not None) - start = time.time() - try: - if settings.page >= 0: - images = [(tif.asarray(key=settings.page), - tif[settings.page])] - elif settings.series >= 0: - images = [(tif.asarray(series=settings.series), - notnone(tif.series[settings.series].pages))] - else: - images = [] - for i, s in enumerate(tif.series): - try: - images.append( - (tif.asarray(series=i), notnone(s.pages))) - except ValueError as e: - images.append((None, notnone(s.pages))) - if settings.debug: - raise - else: - print("\n* series %i failed: %s... " % (i, e), - end='') - print("%.3f ms" % ((time.time()-start) * 1e3)) - except Exception as e: - if settings.debug: - raise - else: - print(e) - - tif.close() - - print("\nTIFF file:", tif) - print() - for i, s in enumerate(tif.series): - print ("Series %i" % i) - print(s) - print() - for i, page in images: - print(page) - print(page.tags) - if page.is_palette: - print("\nColor Map:", page.color_map.shape, page.color_map.dtype) - for attr in ('cz_lsm_info', 'cz_lsm_scan_info', 'uic_tags', - 'mm_header', 'imagej_tags', 'micromanager_metadata', - 'nih_image_header'): - if hasattr(page, attr): - print("", attr.upper(), Record(getattr(page, attr)), sep="\n") - print() - if page.is_micromanager: - print('MICROMANAGER_FILE_METADATA') - print(Record(tif.micromanager_metadata)) - - if images and not settings.noplot: - try: - import matplotlib - matplotlib.use('TkAgg') - from matplotlib import pyplot - except ImportError as e: - warnings.warn("failed to import matplotlib.\n%s" % e) - else: - for img, page in images: - if img is None: - continue - vmin, vmax = None, None - if 'gdal_nodata' in page.tags: - try: - vmin = numpy.min(img[img > float(page.gdal_nodata)]) - except ValueError: - pass - if page.is_stk: - try: - vmin = page.uic_tags['min_scale'] - vmax = page.uic_tags['max_scale'] - except KeyError: - pass - else: - if vmax <= vmin: - vmin, vmax = None, None - title = "%s\n %s" % (str(tif), str(page)) - imshow(img, title=title, vmin=vmin, vmax=vmax, - bitspersample=page.bits_per_sample, - photometric=page.photometric, - interpolation=settings.interpol, - dpi=settings.dpi) - pyplot.show() - - -TIFFfile = TiffFile # backwards compatibility - -if sys.version_info[0] > 2: - basestring = str, bytes - unicode = str - -if __name__ == "__main__": - sys.exit(main()) diff --git a/skimage/io/_plugins/tifffile_plugin.ini b/skimage/io/_plugins/tifffile_plugin.ini deleted file mode 100644 index bf83fce2..00000000 --- a/skimage/io/_plugins/tifffile_plugin.ini +++ /dev/null @@ -1,3 +0,0 @@ -[tifffile] -description = Load and save TIFF and TIFF-based images using tifffile.py -provides = imread, imsave diff --git a/skimage/io/_plugins/tifffile_plugin.py b/skimage/io/_plugins/tifffile_plugin.py deleted file mode 100644 index 58038db4..00000000 --- a/skimage/io/_plugins/tifffile_plugin.py +++ /dev/null @@ -1,6 +0,0 @@ -try: - from tifffile import imread, imsave -except ImportError: - raise ImportError("The tifffile module could not be found.\n" - "It can be obtained at " - "\n") diff --git a/skimage/io/tests/test_tifffile.py b/skimage/io/tests/test_tifffile.py deleted file mode 100644 index 7019f0c6..00000000 --- a/skimage/io/tests/test_tifffile.py +++ /dev/null @@ -1,62 +0,0 @@ -import os -import skimage as si -import skimage.io as sio -import numpy as np - -from numpy.testing import * -from numpy.testing.decorators import skipif -from tempfile import NamedTemporaryFile - -try: - import skimage.io._plugins.tifffile_plugin as tf - _plugins = sio.plugin_order() - TF_available = True - sio.use_plugin('tifffile') -except ImportError: - TF_available = False - -np.random.seed(0) - - -def teardown(): - sio.reset_plugins() - - -@skipif(not TF_available) -def test_imread_uint16(): - expected = np.load(os.path.join(si.data_dir, 'chessboard_GRAY_U8.npy')) - img = sio.imread(os.path.join(si.data_dir, 'chessboard_GRAY_U16.tif')) - assert img.dtype == np.uint16 - assert_array_almost_equal(img, expected) - - -@skipif(not TF_available) -def test_imread_uint16_big_endian(): - expected = np.load(os.path.join(si.data_dir, 'chessboard_GRAY_U8.npy')) - img = sio.imread(os.path.join(si.data_dir, 'chessboard_GRAY_U16B.tif')) - assert img.dtype == np.uint16 - assert_array_almost_equal(img, expected) - - -class TestSave: - def roundtrip(self, dtype, x): - f = NamedTemporaryFile(suffix='.tif') - fname = f.name - f.close() - sio.imsave(fname, x) - y = sio.imread(fname) - assert_array_equal(x, y) - - @skipif(not TF_available) - def test_imsave_roundtrip(self): - for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: - for dtype in (np.uint8, np.uint16, np.float32, np.float64): - x = np.ones(shape, dtype=dtype) * np.random.rand(*shape) - - if not np.issubdtype(dtype, float): - x = (x * 255).astype(dtype) - yield self.roundtrip, dtype, x - - -if __name__ == "__main__": - run_module_suite() From 2e47ca2d6768acd49cd5afd4b07af1ba4f4931cd Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 5 Oct 2014 20:53:34 -0500 Subject: [PATCH 0503/1122] Add tifffile.c and associated build target. --- skimage/io/_plugins/tifffile.c | 962 +++++++++++++++++++++++++++++++++ skimage/io/setup.py | 4 + 2 files changed, 966 insertions(+) create mode 100644 skimage/io/_plugins/tifffile.c diff --git a/skimage/io/_plugins/tifffile.c b/skimage/io/_plugins/tifffile.c new file mode 100644 index 00000000..6aa0eaa1 --- /dev/null +++ b/skimage/io/_plugins/tifffile.c @@ -0,0 +1,962 @@ + + +/* tifffile.c + +A Python C extension module for decoding PackBits and LZW encoded TIFF data. + +Refer to the tifffile.py module for documentation and tests. + +:Author: + `Christoph Gohlke `_ + +:Organization: + Laboratory for Fluorescence Dynamics, University of California, Irvine + +:Version: 2013.11.05 + +Install +------- +Use this Python distutils setup script to build the extension module:: + + # setup.py + # Usage: ``python setup.py build_ext --inplace`` + from distutils.core import setup, Extension + import numpy + setup(name='_tifffile', + ext_modules=[Extension('_tifffile', ['tifffile.c'], + include_dirs=[numpy.get_include()])]) + +License +------- +Copyright (c) 2008-2014, Christoph Gohlke +Copyright (c) 2008-2014, The Regents of the University of California +Produced at the Laboratory for Fluorescence Dynamics +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of the copyright holders nor the names of any + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#define _VERSION_ "2013.11.05" + +#define WIN32_LEAN_AND_MEAN +#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION + +#include "Python.h" +#include "string.h" +#include "numpy/arrayobject.h" + +/* little endian by default */ +#ifndef MSB +#define MSB 1 +#endif + +#if MSB +#define LSB 0 +#define BOC '<' +#else +#define LSB 1 +#define BOC '>' +#endif + +#define NO_ERROR 0 +#define VALUE_ERROR -1 + +#ifdef _MSC_VER +typedef unsigned __int8 uint8_t; +typedef unsigned __int16 uint16_t; +typedef unsigned __int32 uint32_t; +typedef unsigned __int64 uint64_t; +#ifdef _WIN64 +typedef __int64 ssize_t; +typedef signed __int64 intptr_t; +typedef unsigned __int64 uintptr_t; +#define SSIZE_MAX (9223372036854775808) +#else +typedef int ssize_t; +typedef _W64 signed int intptr_t; +typedef _W64 unsigned int uintptr_t; +#define SSIZE_MAX (2147483648) +#endif +#else +/* non MS compilers */ +#include +#include +#endif + +#define SWAP2BYTES(x) \ + ((((x) >> 8) & 0x00FF) | (((x) & 0x00FF) << 8)) + +#define SWAP4BYTES(x) \ + ((((x) >> 24) & 0x00FF) | (((x)&0x00FF) << 24) | \ + (((x) >> 8 ) & 0xFF00) | (((x)&0xFF00) << 8)) + +#define SWAP8BYTES(x) \ + ((((x) >> 56) & 0x00000000000000FF) | (((x) >> 40) & 0x000000000000FF00) | \ + (((x) >> 24) & 0x0000000000FF0000) | (((x) >> 8) & 0x00000000FF000000) | \ + (((x) << 8) & 0x000000FF00000000) | (((x) << 24) & 0x0000FF0000000000) | \ + (((x) << 40) & 0x00FF000000000000) | (((x) << 56) & 0xFF00000000000000)) + +struct BYTE_STRING { + unsigned int ref; /* reference count */ + unsigned int len; /* length of string */ + char *str; /* pointer to bytes */ +}; + +typedef union { + uint8_t b[2]; + uint16_t i; +} u_uint16; + +typedef union { + uint8_t b[4]; + uint32_t i; +} u_uint32; + +typedef union { + uint8_t b[8]; + uint64_t i; +} u_uint64; + +/*****************************************************************************/ +/* C functions */ + +/* Return mask for itemsize bits */ +unsigned char bitmask(const int itemsize) { + unsigned char result = 0; + unsigned char power = 1; + int i; + for (i = 0; i < itemsize; i++) { + result += power; + power *= 2; + } + return result << (8 - itemsize); +} + +/** Unpack sequence of tigthly packed 1-32 bit integers. + +Native byte order will be returned. + +Input data array should be padded to the next 16, 32 or 64-bit boundary +if itemsize not in (1, 2, 4, 8, 16, 24, 32, 64). + +*/ +int unpackbits( + unsigned char *data, + const ssize_t size, /** size of data in bytes */ + const int itemsize, /** number of bits in integer */ + ssize_t numitems, /** number of items to unpack */ + unsigned char *result /** buffer to store unpacked items */ + ) +{ + ssize_t i, j, k, storagesize; + unsigned char value; + /* Input validation is done in wrapper function */ + storagesize = (ssize_t)(ceil(itemsize / 8.0)); + storagesize = storagesize < 3 ? storagesize : storagesize > 4 ? 8 : 4; + switch (itemsize) { + case 8: + case 16: + case 32: + case 64: + memcpy(result, data, numitems*storagesize); + return NO_ERROR; + case 1: + for (i = 0, j = 0; i < numitems/8; i++) { + value = data[i]; + result[j++] = (value & (unsigned char)(128)) >> 7; + result[j++] = (value & (unsigned char)(64)) >> 6; + result[j++] = (value & (unsigned char)(32)) >> 5; + result[j++] = (value & (unsigned char)(16)) >> 4; + result[j++] = (value & (unsigned char)(8)) >> 3; + result[j++] = (value & (unsigned char)(4)) >> 2; + result[j++] = (value & (unsigned char)(2)) >> 1; + result[j++] = (value & (unsigned char)(1)); + } + if (numitems % 8) { + value = data[i]; + switch (numitems % 8) { + case 7: result[j+6] = (value & (unsigned char)(2)) >> 1; + case 6: result[j+5] = (value & (unsigned char)(4)) >> 2; + case 5: result[j+4] = (value & (unsigned char)(8)) >> 3; + case 4: result[j+3] = (value & (unsigned char)(16)) >> 4; + case 3: result[j+2] = (value & (unsigned char)(32)) >> 5; + case 2: result[j+1] = (value & (unsigned char)(64)) >> 6; + case 1: result[j] = (value & (unsigned char)(128)) >> 7; + } + } + return NO_ERROR; + case 2: + for (i = 0, j = 0; i < numitems/4; i++) { + value = data[i]; + result[j++] = (value & (unsigned char)(192)) >> 6; + result[j++] = (value & (unsigned char)(48)) >> 4; + result[j++] = (value & (unsigned char)(12)) >> 2; + result[j++] = (value & (unsigned char)(3)); + } + if (numitems % 4) { + value = data[i]; + switch (numitems % 4) { + case 3: result[j+2] = (value & (unsigned char)(12)) >> 2; + case 2: result[j+1] = (value & (unsigned char)(48)) >> 4; + case 1: result[j] = (value & (unsigned char)(192)) >> 6; + } + } + return NO_ERROR; + case 4: + for (i = 0, j = 0; i < numitems/2; i++) { + value = data[i]; + result[j++] = (value & (unsigned char)(240)) >> 4; + result[j++] = (value & (unsigned char)(15)); + } + if (numitems % 2) { + value = data[i]; + result[j] = (value & (unsigned char)(240)) >> 4; + } + return NO_ERROR; + case 24: + j = k = 0; + for (i = 0; i < numitems; i++) { + result[j++] = 0; + result[j++] = data[k++]; + result[j++] = data[k++]; + result[j++] = data[k++]; + } + return NO_ERROR; + } + /* 3, 5, 6, 7 */ + if (itemsize < 8) { + int shr = 16; + u_uint16 value, mask, tmp; + j = k = 0; + value.b[MSB] = data[j++]; + value.b[LSB] = data[j++]; + mask.b[MSB] = bitmask(itemsize); + mask.b[LSB] = 0; + for (i = 0; i < numitems; i++) { + shr -= itemsize; + tmp.i = (value.i & mask.i) >> shr; + result[k++] = tmp.b[LSB]; + if (shr < itemsize) { + value.b[MSB] = value.b[LSB]; + value.b[LSB] = data[j++]; + mask.i <<= 8 - itemsize; + shr += 8; + } else { + mask.i >>= itemsize; + } + } + return NO_ERROR; + } + /* 9, 10, 11, 12, 13, 14, 15 */ + if (itemsize < 16) { + int shr = 32; + u_uint32 value, mask, tmp; + mask.i = 0; + j = k = 0; +#if MSB + for (i = 3; i >= 0; i--) { + value.b[i] = data[j++]; + } + mask.b[3] = 0xFF; + mask.b[2] = bitmask(itemsize-8); + for (i = 0; i < numitems; i++) { + shr -= itemsize; + tmp.i = (value.i & mask.i) >> shr; + result[k++] = tmp.b[0]; /* swap bytes */ + result[k++] = tmp.b[1]; + if (shr < itemsize) { + value.b[3] = value.b[1]; + value.b[2] = value.b[0]; + value.b[1] = data[j++]; + value.b[0] = data[j++]; + mask.i <<= 16 - itemsize; + shr += 16; + } else { + mask.i >>= itemsize; + } + } +#else + /* not implemented */ +#endif + return NO_ERROR; + } + /* 17, 18, 19, 20, 21, 22, 23, 25, 26, 27, 28, 29, 30, 31 */ + if (itemsize < 32) { + int shr = 64; + u_uint64 value, mask, tmp; + mask.i = 0; + j = k = 0; +#if MSB + for (i = 7; i >= 0; i--) { + value.b[i] = data[j++]; + } + mask.b[7] = 0xFF; + mask.b[6] = 0xFF; + mask.b[5] = itemsize > 23 ? 0xFF : bitmask(itemsize-16); + mask.b[4] = itemsize < 24 ? 0x00 : bitmask(itemsize-24); + for (i = 0; i < numitems; i++) { + shr -= itemsize; + tmp.i = (value.i & mask.i) >> shr; + result[k++] = tmp.b[0]; /* swap bytes */ + result[k++] = tmp.b[1]; + result[k++] = tmp.b[2]; + result[k++] = tmp.b[3]; + if (shr < itemsize) { + value.b[7] = value.b[3]; + value.b[6] = value.b[2]; + value.b[5] = value.b[1]; + value.b[4] = value.b[0]; + value.b[3] = data[j++]; + value.b[2] = data[j++]; + value.b[1] = data[j++]; + value.b[0] = data[j++]; + mask.i <<= 32 - itemsize; + shr += 32; + } else { + mask.i >>= itemsize; + } + } +#else + /* Not implemented */ +#endif + return NO_ERROR; + } + return VALUE_ERROR; +} + +/*****************************************************************************/ +/* Python functions */ + +/** Unpack tightly packed integers. */ +char py_unpackints_doc[] = "Unpack groups of bits into numpy array."; + +static PyObject* +py_unpackints(PyObject *obj, PyObject *args, PyObject *kwds) +{ + PyObject *byteobj = NULL; + PyArrayObject *result = NULL; + PyArray_Descr *dtype = NULL; + char *encoded = NULL; + char *decoded = NULL; + Py_ssize_t encoded_len = 0; + Py_ssize_t decoded_len = 0; + Py_ssize_t runlen = 0; + Py_ssize_t i; + int storagesize, bytesize; + int itemsize = 0; + int skipbits = 0; + static char *kwlist[] = {"data", "dtype", "itemsize", "runlen", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO&i|i", kwlist, + &byteobj, PyArray_DescrConverter, &dtype, &itemsize, &runlen)) + return NULL; + + Py_INCREF(byteobj); + + if (((itemsize < 1) || (itemsize > 32)) && (itemsize != 64)) { + PyErr_Format(PyExc_ValueError, "itemsize out of range"); + goto _fail; + } + + if (!PyBytes_Check(byteobj)) { + PyErr_Format(PyExc_TypeError, "expected byte string as input"); + goto _fail; + } + + encoded = PyBytes_AS_STRING(byteobj); + encoded_len = PyBytes_GET_SIZE(byteobj); + bytesize = (int)ceil(itemsize / 8.0); + storagesize = bytesize < 3 ? bytesize : bytesize > 4 ? 8 : 4; + if ((encoded_len < bytesize) || (encoded_len > SSIZE_MAX / storagesize)) { + PyErr_Format(PyExc_ValueError, "data size out of range"); + goto _fail; + } + if (dtype->elsize != storagesize) { + PyErr_Format(PyExc_TypeError, "dtype.elsize doesn't fit itemsize"); + goto _fail; + } + + if (runlen == 0) { + runlen = (Py_ssize_t)(((uint64_t)encoded_len*8) / (uint64_t)itemsize); + } + skipbits = (Py_ssize_t)(((uint64_t)runlen * (uint64_t)itemsize) % 8); + if (skipbits > 0) { + skipbits = 8 - skipbits; + } + decoded_len = (Py_ssize_t)((uint64_t)runlen * (((uint64_t)encoded_len*8) / + ((uint64_t)runlen*(uint64_t)itemsize + (uint64_t)skipbits))); + + result = (PyArrayObject *)PyArray_SimpleNew(1, &decoded_len, + dtype->type_num); + if (result == NULL) { + PyErr_Format(PyExc_MemoryError, "unable to allocate output array"); + goto _fail; + } + decoded = (char *)PyArray_DATA(result); + + for (i = 0; i < decoded_len; i+=runlen) { + if (NO_ERROR != + unpackbits((unsigned char *) encoded, + (ssize_t) encoded_len, + (int) itemsize, + (ssize_t) runlen, + (unsigned char *) decoded)) { + PyErr_Format(PyExc_ValueError, "unpackbits() failed"); + goto _fail; + } + encoded += (Py_ssize_t)(((uint64_t)runlen * (uint64_t)itemsize + + (uint64_t)skipbits) / 8); + decoded += runlen * storagesize; + } + + if ((dtype->byteorder != BOC) && (itemsize % 8 == 0)) { + switch (dtype->elsize) { + case 2: { + uint16_t *d = (uint16_t *)PyArray_DATA(result); + for (i = 0; i < PyArray_SIZE(result); i++) { + *d = SWAP2BYTES(*d); d++; + } + break; } + case 4: { + uint32_t *d = (uint32_t *)PyArray_DATA(result); + for (i = 0; i < PyArray_SIZE(result); i++) { + *d = SWAP4BYTES(*d); d++; + } + break; } + case 8: { + uint64_t *d = (uint64_t *)PyArray_DATA(result); + for (i = 0; i < PyArray_SIZE(result); i++) { + *d = SWAP8BYTES(*d); d++; + } + break; } + } + } + Py_DECREF(byteobj); + Py_DECREF(dtype); + return PyArray_Return(result); + + _fail: + Py_XDECREF(byteobj); + Py_XDECREF(result); + Py_XDECREF(dtype); + return NULL; +} + + +/** Decode TIFF PackBits encoded string. */ +char py_decodepackbits_doc[] = "Return TIFF PackBits decoded string."; + +static PyObject * +py_decodepackbits(PyObject *obj, PyObject *args) +{ + int n; + char e; + char *decoded = NULL; + char *encoded = NULL; + char *encoded_end = NULL; + char *encoded_pos = NULL; + unsigned int encoded_len; + unsigned int decoded_len; + PyObject *byteobj = NULL; + PyObject *result = NULL; + + if (!PyArg_ParseTuple(args, "O", &byteobj)) + return NULL; + + if (!PyBytes_Check(byteobj)) { + PyErr_Format(PyExc_TypeError, "expected byte string as input"); + goto _fail; + } + + Py_INCREF(byteobj); + encoded = PyBytes_AS_STRING(byteobj); + encoded_len = (unsigned int)PyBytes_GET_SIZE(byteobj); + + /* release GIL: byte/string objects are immutable */ + Py_BEGIN_ALLOW_THREADS + + /* determine size of decoded string */ + encoded_pos = encoded; + encoded_end = encoded + encoded_len; + decoded_len = 0; + while (encoded_pos < encoded_end) { + n = (int)*encoded_pos++; + if (n >= 0) { + n++; + if (encoded_pos+n > encoded_end) + n = (int)(encoded_end - encoded_pos); + encoded_pos += n; + decoded_len += n; + } else if (n > -128) { + encoded_pos++; + decoded_len += 1-n; + } + } + Py_END_ALLOW_THREADS + + result = PyBytes_FromStringAndSize(0, decoded_len); + if (result == NULL) { + PyErr_Format(PyExc_MemoryError, "failed to allocate decoded string"); + goto _fail; + } + decoded = PyBytes_AS_STRING(result); + + Py_BEGIN_ALLOW_THREADS + + /* decode string */ + encoded_end = encoded + encoded_len; + while (encoded < encoded_end) { + n = (int)*encoded++; + if (n >= 0) { + n++; + if (encoded+n > encoded_end) + n = (int)(encoded_end - encoded); + /* memmove(decoded, encoded, n); decoded += n; encoded += n; */ + while (n--) + *decoded++ = *encoded++; + } else if (n > -128) { + n = 1 - n; + e = *encoded++; + /* memset(decoded, e, n); decoded += n; */ + while (n--) + *decoded++ = e; + } + } + Py_END_ALLOW_THREADS + + Py_DECREF(byteobj); + return result; + + _fail: + Py_XDECREF(byteobj); + Py_XDECREF(result); + return NULL; +} + + +/** Decode TIFF LZW encoded string. */ +char py_decodelzw_doc[] = "Return TIFF LZW decoded string."; + +static PyObject * +py_decodelzw(PyObject *obj, PyObject *args) +{ + PyThreadState *_save = NULL; + PyObject *byteobj = NULL; + PyObject *result = NULL; + int i, j; + unsigned int encoded_len = 0; + unsigned int decoded_len = 0; + unsigned int result_len = 0; + unsigned int table_len = 0; + unsigned int len; + unsigned int code, c, oldcode, mask, shr; + uint64_t bitcount, bitw; + char *encoded = NULL; + char *result_ptr = NULL; + char *table2 = NULL; + char *cptr; + struct BYTE_STRING *decoded = NULL; + struct BYTE_STRING *decoded_ptr = NULL; + struct BYTE_STRING *table[4096]; + struct BYTE_STRING *newentry, *newresult, *t; + int little_endian = 0; + + if (!PyArg_ParseTuple(args, "O", &byteobj)) + return NULL; + + if (!PyBytes_Check(byteobj)) { + PyErr_Format(PyExc_TypeError, "expected byte string as input"); + goto _fail; + } + + Py_INCREF(byteobj); + encoded = PyBytes_AS_STRING(byteobj); + encoded_len = (unsigned int)PyBytes_GET_SIZE(byteobj); + /* + if (encoded_len >= 512 * 1024 * 1024) { + PyErr_Format(PyExc_ValueError, "encoded data > 512 MB not supported"); + goto _fail; + } + */ + /* release GIL: byte/string objects are immutable */ + _save = PyEval_SaveThread(); + + if ((*encoded != -128) || ((*(encoded+1) & 128))) { + PyEval_RestoreThread(_save); + PyErr_Format(PyExc_ValueError, + "strip must begin with CLEAR code"); + goto _fail; + } + little_endian = (*(unsigned short *)encoded) & 128; + + /* allocate buffer for codes and pointers */ + decoded_len = 0; + len = (encoded_len + encoded_len/9) * sizeof(decoded); + decoded = PyMem_Malloc(len * sizeof(void *)); + if (decoded == NULL) { + PyEval_RestoreThread(_save); + PyErr_Format(PyExc_MemoryError, "failed to allocate decoded"); + goto _fail; + } + memset((void *)decoded, 0, len * sizeof(void *)); + decoded_ptr = decoded; + + /* cache strings of length 2 */ + cptr = table2 = PyMem_Malloc(256*256*2 * sizeof(char)); + if (table2 == NULL) { + PyEval_RestoreThread(_save); + PyErr_Format(PyExc_MemoryError, "failed to allocate table2"); + goto _fail; + } + for (i = 0; i < 256; i++) { + for (j = 0; j < 256; j++) { + *cptr++ = (char)i; + *cptr++ = (char)j; + } + } + + memset(table, 0, sizeof(table)); + table_len = 258; + bitw = 9; + shr = 23; + mask = 4286578688; + bitcount = 0; + result_len = 0; + code = 0; + oldcode = 0; + + while ((unsigned int)((bitcount + bitw) / 8) <= encoded_len) { + /* read next code */ + code = *((unsigned int *)((void *)(encoded + (bitcount / 8)))); + if (little_endian) + code = SWAP4BYTES(code); + code <<= (unsigned int)(bitcount % 8); + code &= mask; + code >>= shr; + bitcount += bitw; + + if (code == 257) /* end of information */ + break; + + if (code == 256) { /* clearcode */ + /* initialize table and switch to 9 bit */ + while (table_len > 258) { + t = table[--table_len]; + t->ref--; + if (t->ref == 0) { + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + } + bitw = 9; + shr = 23; + mask = 4286578688; + + /* read next code */ + code = *((unsigned int *)((void *)(encoded + (bitcount / 8)))); + if (little_endian) + code = SWAP4BYTES(code); + code <<= bitcount % 8; + code &= mask; + code >>= shr; + bitcount += bitw; + + if (code == 257) /* end of information */ + break; + + /* decoded.append(table[code]) */ + if (code < 256) { + result_len++; + *((int *)decoded_ptr++) = code; + } else { + newresult = table[code]; + newresult->ref++; + result_len += newresult->len; + *(struct BYTE_STRING **)decoded_ptr++ = newresult; + } + } else { + if (code < table_len) { + /* code is in table */ + /* newresult = table[code]; */ + /* newentry = table[oldcode] + table[code][0] */ + /* decoded.append(newresult); table.append(newentry) */ + if (code < 256) { + c = code; + *((unsigned int *)decoded_ptr++) = code; + result_len++; + } else { + newresult = table[code]; + newresult->ref++; + c = (unsigned int) *newresult->str; + *(struct BYTE_STRING **)decoded_ptr++ = newresult; + result_len += newresult->len; + } + newentry = PyMem_Malloc(sizeof(struct BYTE_STRING)); + newentry->ref = 1; + if (oldcode < 256) { + newentry->len = 2; + newentry->str = table2 + (oldcode << 9) + + ((unsigned char)c << 1); + } else { + len = table[oldcode]->len; + newentry->len = len + 1; + newentry->str = PyMem_Malloc(newentry->len); + if (newentry->str == NULL) + break; + memmove(newentry->str, table[oldcode]->str, len); + newentry->str[len] = c; + } + table[table_len++] = newentry; + } else { + /* code is not in table */ + /* newentry = newresult = table[oldcode] + table[oldcode][0] */ + /* decoded.append(newresult); table.append(newentry) */ + newresult = PyMem_Malloc(sizeof(struct BYTE_STRING)); + newentry = newresult; + newentry->ref = 2; + if (oldcode < 256) { + newentry->len = 2; + newentry->str = table2 + 514*oldcode; + } else { + len = table[oldcode]->len; + newentry->len = len + 1; + newentry->str = PyMem_Malloc(newentry->len); + if (newentry->str == NULL) + break; + memmove(newentry->str, table[oldcode]->str, len); + newentry->str[len] = *table[oldcode]->str; + } + table[table_len++] = newentry; + *(struct BYTE_STRING **)decoded_ptr++ = newresult; + result_len += newresult->len; + } + } + oldcode = code; + /* increase bit-width if necessary */ + switch (table_len) { + case 511: + bitw = 10; + shr = 22; + mask = 4290772992; + break; + case 1023: + bitw = 11; + shr = 21; + mask = 4292870144; + break; + case 2047: + bitw = 12; + shr = 20; + mask = 4293918720; + } + } + + PyEval_RestoreThread(_save); + + if (code != 257) { + PyErr_WarnEx(NULL, + "py_decodelzw encountered unexpected end of stream", 1); + } + + /* result = ''.join(decoded) */ + decoded_len = (unsigned int)(decoded_ptr - decoded); + decoded_ptr = decoded; + result = PyBytes_FromStringAndSize(0, result_len); + if (result == NULL) { + PyErr_Format(PyExc_MemoryError, "failed to allocate decoded string"); + goto _fail; + } + result_ptr = PyBytes_AS_STRING(result); + + _save = PyEval_SaveThread(); + + while (decoded_len--) { + code = *((unsigned int *)decoded_ptr); + if (code < 256) { + *result_ptr++ = (char)code; + } else { + t = *((struct BYTE_STRING **)decoded_ptr); + memmove(result_ptr, t->str, t->len); + result_ptr += t->len; + if (--t->ref == 0) { + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + } + decoded_ptr++; + } + PyMem_Free(decoded); + + while (table_len-- > 258) { + t = table[table_len]; + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + PyMem_Free(table2); + + PyEval_RestoreThread(_save); + + Py_DECREF(byteobj); + return result; + + _fail: + if (table2 != NULL) + PyMem_Free(table2); + if (decoded != NULL) { + /* Bug? are decoded_ptr and decoded_len correct? */ + while (decoded_len--) { + code = *((unsigned int *) decoded_ptr); + if (code > 258) { + t = *((struct BYTE_STRING **) decoded_ptr); + if (--t->ref == 0) { + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + } + } + PyMem_Free(decoded); + } + while (table_len-- > 258) { + t = table[table_len]; + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + + Py_XDECREF(byteobj); + Py_XDECREF(result); + + return NULL; +} + +/*****************************************************************************/ +/* Create Python module */ + +char module_doc[] = + "A Python C extension module for decoding PackBits and LZW encoded " + "TIFF data.\n\n" + "Refer to the tifffile.py module for documentation and tests.\n\n" + "Authors:\n Christoph Gohlke \n" + " Laboratory for Fluorescence Dynamics, University of California, Irvine." + "\n\nVersion: %s\n"; + +static PyMethodDef module_methods[] = { +#if MSB + {"unpackints", (PyCFunction)py_unpackints, METH_VARARGS|METH_KEYWORDS, + py_unpackints_doc}, +#endif + {"decodelzw", (PyCFunction)py_decodelzw, METH_VARARGS, + py_decodelzw_doc}, + {"decodepackbits", (PyCFunction)py_decodepackbits, METH_VARARGS, + py_decodepackbits_doc}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +#if PY_MAJOR_VERSION >= 3 + +struct module_state { + PyObject *error; +}; + +#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) + +static int module_traverse(PyObject *m, visitproc visit, void *arg) { + Py_VISIT(GETSTATE(m)->error); + return 0; +} + +static int module_clear(PyObject *m) { + Py_CLEAR(GETSTATE(m)->error); + return 0; +} + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_tifffile", + NULL, + sizeof(struct module_state), + module_methods, + NULL, + module_traverse, + module_clear, + NULL +}; + +#define INITERROR return NULL + +PyMODINIT_FUNC +PyInit__tifffile(void) + +#else + +#define INITERROR return + +PyMODINIT_FUNC +init_tifffile(void) + +#endif +{ + PyObject *module; + + char *doc = (char *)PyMem_Malloc(sizeof(module_doc) + sizeof(_VERSION_)); + PyOS_snprintf(doc, sizeof(doc), module_doc, _VERSION_); + +#if PY_MAJOR_VERSION >= 3 + moduledef.m_doc = doc; + module = PyModule_Create(&moduledef); +#else + module = Py_InitModule3("_tifffile", module_methods, doc); +#endif + + PyMem_Free(doc); + + if (module == NULL) + INITERROR; + + if (_import_array() < 0) { + Py_DECREF(module); + INITERROR; + } + + { +#if PY_MAJOR_VERSION < 3 + PyObject *s = PyString_FromString(_VERSION_); +#else + PyObject *s = PyUnicode_FromString(_VERSION_); +#endif + PyObject *dict = PyModule_GetDict(module); + PyDict_SetItemString(dict, "__version__", s); + Py_DECREF(s); + } + +#if PY_MAJOR_VERSION >= 3 + return module; +#endif +} + diff --git a/skimage/io/setup.py b/skimage/io/setup.py index 4e3c09c3..dbe7e940 100644 --- a/skimage/io/setup.py +++ b/skimage/io/setup.py @@ -27,6 +27,10 @@ def configuration(parent_package='', top_path=None): sources=['_plugins/_histograms.c'], include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_plugins._tifffile', + sources=['_plugins/tifffile.c'], + include_dirs=[get_numpy_include_dirs()]) + return config if __name__ == '__main__': From 199c3da3078a57b05880ce2deef0ad6d5a0cc87b Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 5 Oct 2014 20:54:13 -0500 Subject: [PATCH 0504/1122] Clean up tests and add to bento.info. Remove "no plugin" test because we will always have a plugin. Fix handling of bools in ubyte check Fix integration of tifffile.c Clean up new tests. Fix _tifffile import and add to bento.info Add tifffile.py and fix pil_plugin import Add tifffile tests to test_pil and remove PIL import checks --- bento.info | 3 + skimage/io/_plugins/pil_plugin.py | 25 +- skimage/io/_plugins/tifffile.py | 4847 +++++++++++++++++++++++++++++ skimage/io/tests/test_io.py | 12 - skimage/io/tests/test_pil.py | 55 +- skimage/io/tests/utils.py | 2 +- 6 files changed, 4891 insertions(+), 53 deletions(-) create mode 100644 skimage/io/_plugins/tifffile.py diff --git a/bento.info b/bento.info index 564f1f5a..bcec295d 100644 --- a/bento.info +++ b/bento.info @@ -157,6 +157,9 @@ Library: Extension: skimage.graph._ncut_cy Sources: skimage/graph/_ncut_cy.pyx + Extension: skimage.io._plugins._tifffile + Sources: + skimage/io/_plugins/tifffile.c Executable: skivi Module: skimage.scripts.skivi diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 2d548533..33ac5bf0 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -14,7 +14,7 @@ except ImportError: from skimage.util import img_as_ubyte, img_as_uint, img_as_int from six import string_types -from tifffile import imread as tif_imread, imsave as tif_imsave +from .tifffile import imread as tif_imread, imsave as tif_imsave def imread(fname, dtype=None): @@ -29,13 +29,9 @@ def imread(fname, dtype=None): """ - if hasattr(fname, 'name'): - name = fname.name.lower() - else: - name = fname.lower() - - if name.endswith(('.tiff', '.tif')) and dtype is None: - return tif_imread(fname) + if hasattr(fname, 'lower') and dtype is None: + if fname.lower().endswith(('.tiff', '.tif')): + return tif_imread(fname) im = Image.open(fname) try: @@ -185,12 +181,15 @@ def imsave(fname, arr, format_str=None): if arr.dtype.kind == 'b': arr = arr.astype(np.uint8) - if hasattr(fname, 'name'): - name = fname.name.lower() - else: - name = fname.lower() + use_tif = False + if hasattr(fname, 'lower'): + if fname.lower().endswith(('.tiff', '.tif')): + use_tif = True + if not format_str is None: + if format_str.lower() in ['tiff', 'tif']: + use_tif = True - if name.endswith(('.tiff', '.tif')): + if use_tif: tif_imsave(fname, arr) else: diff --git a/skimage/io/_plugins/tifffile.py b/skimage/io/_plugins/tifffile.py new file mode 100644 index 00000000..11744289 --- /dev/null +++ b/skimage/io/_plugins/tifffile.py @@ -0,0 +1,4847 @@ + + +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# tifffile.py + +# Copyright (c) 2008-2014, Christoph Gohlke +# Copyright (c) 2008-2014, The Regents of the University of California +# Produced at the Laboratory for Fluorescence Dynamics +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the copyright holders nor the names of any +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +"""Read and write image data from and to TIFF files. + +Image and metadata can be read from TIFF, BigTIFF, OME-TIFF, STK, LSM, NIH, +SGI, ImageJ, MicroManager, FluoView, SEQ and GEL files. +Only a subset of the TIFF specification is supported, mainly uncompressed +and losslessly compressed 2**(0 to 6) bit integer, 16, 32 and 64-bit float, +grayscale and RGB(A) images, which are commonly used in bio-scientific imaging. +Specifically, reading JPEG and CCITT compressed image data or EXIF, IPTC, GPS, +and XMP metadata is not implemented. +Only primary info records are read for STK, FluoView, MicroManager, and +NIH image formats. + +TIFF, the Tagged Image File Format, is under the control of Adobe Systems. +BigTIFF allows for files greater than 4 GB. STK, LSM, FluoView, SGI, SEQ, GEL, +and OME-TIFF, are custom extensions defined by Molecular Devices (Universal +Imaging Corporation), Carl Zeiss MicroImaging, Olympus, Silicon Graphics +International, Media Cybernetics, Molecular Dynamics, and the Open Microscopy +Environment consortium respectively. + +For command line usage run ``python tifffile.py --help`` + +:Author: + `Christoph Gohlke `_ + +:Organization: + Laboratory for Fluorescence Dynamics, University of California, Irvine + +:Version: 2014.08.24 + +Requirements +------------ +* `CPython 2.7 or 3.4 `_ +* `Numpy 1.8.2 `_ +* `Matplotlib 1.4 `_ (optional for plotting) +* `Tifffile.c 2013.11.05 `_ + (recommended for faster decoding of PackBits and LZW encoded strings) + +Notes +----- +The API is not stable yet and might change between revisions. + +Tested on little-endian platforms only. + +Other Python packages and modules for reading bio-scientific TIFF files: + +* `Imread `_ +* `PyLibTiff `_ +* `SimpleITK `_ +* `PyLSM `_ +* `PyMca.TiffIO.py `_ (same as fabio.TiffIO) +* `BioImageXD.Readers `_ +* `Cellcognition.io `_ +* `CellProfiler.bioformats + `_ + +Acknowledgements +---------------- +* Egor Zindy, University of Manchester, for cz_lsm_scan_info specifics. +* Wim Lewis for a bug fix and some read_cz_lsm functions. +* Hadrien Mary for help on reading MicroManager files. + +References +---------- +(1) TIFF 6.0 Specification and Supplements. Adobe Systems Incorporated. + http://partners.adobe.com/public/developer/tiff/ +(2) TIFF File Format FAQ. http://www.awaresystems.be/imaging/tiff/faq.html +(3) MetaMorph Stack (STK) Image File Format. + http://support.meta.moleculardevices.com/docs/t10243.pdf +(4) Image File Format Description LSM 5/7 Release 6.0 (ZEN 2010). + Carl Zeiss MicroImaging GmbH. BioSciences. May 10, 2011 +(5) File Format Description - LSM 5xx Release 2.0. + http://ibb.gsf.de/homepage/karsten.rodenacker/IDL/Lsmfile.doc +(6) The OME-TIFF format. + http://www.openmicroscopy.org/site/support/file-formats/ome-tiff +(7) UltraQuant(r) Version 6.0 for Windows Start-Up Guide. + http://www.ultralum.com/images%20ultralum/pdf/UQStart%20Up%20Guide.pdf +(8) Micro-Manager File Formats. + http://www.micro-manager.org/wiki/Micro-Manager_File_Formats +(9) Tags for TIFF and Related Specifications. Digital Preservation. + http://www.digitalpreservation.gov/formats/content/tiff_tags.shtml + +Examples +-------- +>>> data = numpy.random.rand(5, 301, 219) +>>> imsave('temp.tif', data) + +>>> image = imread('temp.tif') +>>> numpy.testing.assert_array_equal(image, data) + +>>> with TiffFile('temp.tif') as tif: +... images = tif.asarray() +... for page in tif: +... for tag in page.tags.values(): +... t = tag.name, tag.value +... image = page.asarray() + +""" + +from __future__ import division, print_function + +import sys +import os +import re +import glob +import math +import zlib +import time +import json +import struct +import warnings +import tempfile +import datetime +import collections +from fractions import Fraction +from xml.etree import cElementTree as etree + +import numpy + +try: + from . import _tifffile +except ImportError: + warnings.warn( + "failed to import the optional _tifffile C extension module.\n" + "Loading of some compressed images will be slow.\n" + "Tifffile.c can be obtained at http://www.lfd.uci.edu/~gohlke/") + +__version__ = '2014.08.24' +__docformat__ = 'restructuredtext en' +__all__ = ('imsave', 'imread', 'imshow', 'TiffFile', 'TiffWriter', + 'TiffSequence') + + +def imsave(filename, data, **kwargs): + """Write image data to TIFF file. + + Refer to the TiffWriter class and member functions for documentation. + + Parameters + ---------- + filename : str + Name of file to write. + data : array_like + Input image. The last dimensions are assumed to be image depth, + height, width, and samples. + kwargs : dict + Parameters 'byteorder', 'bigtiff', and 'software' are passed to + the TiffWriter class. + Parameters 'photometric', 'planarconfig', 'resolution', + 'description', 'compress', 'volume', and 'extratags' are passed to + the TiffWriter.save function. + + Examples + -------- + >>> data = numpy.random.rand(2, 5, 3, 301, 219) + >>> description = u'{"shape": %s}' % str(list(data.shape)) + >>> imsave('temp.tif', data, compress=6, + ... extratags=[(270, 's', 0, description, True)]) + + """ + tifargs = {} + for key in ('byteorder', 'bigtiff', 'software', 'writeshape'): + if key in kwargs: + tifargs[key] = kwargs[key] + del kwargs[key] + + if 'writeshape' not in kwargs: + kwargs['writeshape'] = True + if 'bigtiff' not in tifargs and data.size*data.dtype.itemsize > 2000*2**20: + tifargs['bigtiff'] = True + + with TiffWriter(filename, **tifargs) as tif: + tif.save(data, **kwargs) + + +class TiffWriter(object): + """Write image data to TIFF file. + + TiffWriter instances must be closed using the close method, which is + automatically called when using the 'with' statement. + + Examples + -------- + >>> data = numpy.random.rand(2, 5, 3, 301, 219) + >>> with TiffWriter('temp.tif', bigtiff=True) as tif: + ... for i in range(data.shape[0]): + ... tif.save(data[i], compress=6) + + """ + TYPES = {'B': 1, 's': 2, 'H': 3, 'I': 4, '2I': 5, 'b': 6, + 'h': 8, 'i': 9, 'f': 11, 'd': 12, 'Q': 16, 'q': 17} + TAGS = { + 'new_subfile_type': 254, 'subfile_type': 255, + 'image_width': 256, 'image_length': 257, 'bits_per_sample': 258, + 'compression': 259, 'photometric': 262, 'fill_order': 266, + 'document_name': 269, 'image_description': 270, 'strip_offsets': 273, + 'orientation': 274, 'samples_per_pixel': 277, 'rows_per_strip': 278, + 'strip_byte_counts': 279, 'x_resolution': 282, 'y_resolution': 283, + 'planar_configuration': 284, 'page_name': 285, 'resolution_unit': 296, + 'software': 305, 'datetime': 306, 'predictor': 317, 'color_map': 320, + 'tile_width': 322, 'tile_length': 323, 'tile_offsets': 324, + 'tile_byte_counts': 325, 'extra_samples': 338, 'sample_format': 339, + 'image_depth': 32997, 'tile_depth': 32998} + + def __init__(self, filename, bigtiff=False, byteorder=None, + software='tifffile.py'): + """Create a new TIFF file for writing. + + Use bigtiff=True when creating files greater than 2 GB. + + Parameters + ---------- + filename : str + Name of file to write. + bigtiff : bool + If True, the BigTIFF format is used. + byteorder : {'<', '>'} + The endianness of the data in the file. + By default this is the system's native byte order. + software : str + Name of the software used to create the image. + Saved with the first page only. + + """ + if byteorder not in (None, '<', '>'): + raise ValueError("invalid byteorder %s" % byteorder) + if byteorder is None: + byteorder = '<' if sys.byteorder == 'little' else '>' + + self._byteorder = byteorder + self._software = software + + self._fh = open(filename, 'wb') + self._fh.write({'<': b'II', '>': b'MM'}[byteorder]) + + if bigtiff: + self._bigtiff = True + self._offset_size = 8 + self._tag_size = 20 + self._numtag_format = 'Q' + self._offset_format = 'Q' + self._val_format = '8s' + self._fh.write(struct.pack(byteorder+'HHH', 43, 8, 0)) + else: + self._bigtiff = False + self._offset_size = 4 + self._tag_size = 12 + self._numtag_format = 'H' + self._offset_format = 'I' + self._val_format = '4s' + self._fh.write(struct.pack(byteorder+'H', 42)) + + # first IFD + self._ifd_offset = self._fh.tell() + self._fh.write(struct.pack(byteorder+self._offset_format, 0)) + + def save(self, data, photometric=None, planarconfig=None, resolution=None, + description=None, volume=False, writeshape=False, compress=0, + extratags=()): + """Write image data to TIFF file. + + Image data are written in one stripe per plane. + Dimensions larger than 2 to 4 (depending on photometric mode, planar + configuration, and SGI mode) are flattened and saved as separate pages. + The 'sample_format' and 'bits_per_sample' TIFF tags are derived from + the data type. + + Parameters + ---------- + data : array_like + Input image. The last dimensions are assumed to be image depth, + height, width, and samples. + photometric : {'minisblack', 'miniswhite', 'rgb'} + The color space of the image data. + By default this setting is inferred from the data shape. + planarconfig : {'contig', 'planar'} + Specifies if samples are stored contiguous or in separate planes. + By default this setting is inferred from the data shape. + 'contig': last dimension contains samples. + 'planar': third last dimension contains samples. + resolution : (float, float) or ((int, int), (int, int)) + X and Y resolution in dots per inch as float or rational numbers. + description : str + The subject of the image. Saved with the first page only. + compress : int + Values from 0 to 9 controlling the level of zlib compression. + If 0, data are written uncompressed (default). + volume : bool + If True, volume data are stored in one tile (if applicable) using + the SGI image_depth and tile_depth tags. + Image width and depth must be multiple of 16. + Few software can read this format, e.g. MeVisLab. + writeshape : bool + If True, write the data shape to the image_description tag + if necessary and no other description is given. + extratags: sequence of tuples + Additional tags as [(code, dtype, count, value, writeonce)]. + + code : int + The TIFF tag Id. + dtype : str + Data type of items in 'value' in Python struct format. + One of B, s, H, I, 2I, b, h, i, f, d, Q, or q. + count : int + Number of data values. Not used for string values. + value : sequence + 'Count' values compatible with 'dtype'. + writeonce : bool + If True, the tag is written to the first page only. + + """ + if photometric not in (None, 'minisblack', 'miniswhite', 'rgb'): + raise ValueError("invalid photometric %s" % photometric) + if planarconfig not in (None, 'contig', 'planar'): + raise ValueError("invalid planarconfig %s" % planarconfig) + if not 0 <= compress <= 9: + raise ValueError("invalid compression level %s" % compress) + + fh = self._fh + byteorder = self._byteorder + numtag_format = self._numtag_format + val_format = self._val_format + offset_format = self._offset_format + offset_size = self._offset_size + tag_size = self._tag_size + + data = numpy.asarray(data, dtype=byteorder+data.dtype.char, order='C') + data_shape = shape = data.shape + data = numpy.atleast_2d(data) + + # normalize shape of data + samplesperpixel = 1 + extrasamples = 0 + if volume and data.ndim < 3: + volume = False + if photometric is None: + if planarconfig: + photometric = 'rgb' + elif data.ndim > 2 and shape[-1] in (3, 4): + photometric = 'rgb' + elif volume and data.ndim > 3 and shape[-4] in (3, 4): + photometric = 'rgb' + elif data.ndim > 2 and shape[-3] in (3, 4): + photometric = 'rgb' + else: + photometric = 'minisblack' + if planarconfig and len(shape) <= (3 if volume else 2): + planarconfig = None + photometric = 'minisblack' + if photometric == 'rgb': + if len(shape) < 3: + raise ValueError("not a RGB(A) image") + if len(shape) < 4: + volume = False + if planarconfig is None: + if shape[-1] in (3, 4): + planarconfig = 'contig' + elif shape[-4 if volume else -3] in (3, 4): + planarconfig = 'planar' + elif shape[-1] > shape[-4 if volume else -3]: + planarconfig = 'planar' + else: + planarconfig = 'contig' + if planarconfig == 'contig': + data = data.reshape((-1, 1) + shape[(-4 if volume else -3):]) + samplesperpixel = data.shape[-1] + else: + data = data.reshape( + (-1,) + shape[(-4 if volume else -3):] + (1,)) + samplesperpixel = data.shape[1] + if samplesperpixel > 3: + extrasamples = samplesperpixel - 3 + elif planarconfig and len(shape) > (3 if volume else 2): + if planarconfig == 'contig': + data = data.reshape((-1, 1) + shape[(-4 if volume else -3):]) + samplesperpixel = data.shape[-1] + else: + data = data.reshape( + (-1,) + shape[(-4 if volume else -3):] + (1,)) + samplesperpixel = data.shape[1] + extrasamples = samplesperpixel - 1 + else: + planarconfig = None + # remove trailing 1s + while len(shape) > 2 and shape[-1] == 1: + shape = shape[:-1] + if len(shape) < 3: + volume = False + if False and ( + len(shape) > (3 if volume else 2) and shape[-1] < 5 and + all(shape[-1] < i + for i in shape[(-4 if volume else -3):-1])): + # DISABLED: non-standard TIFF, e.g. (220, 320, 2) + planarconfig = 'contig' + samplesperpixel = shape[-1] + data = data.reshape((-1, 1) + shape[(-4 if volume else -3):]) + else: + data = data.reshape( + (-1, 1) + shape[(-3 if volume else -2):] + (1,)) + + if samplesperpixel == 2: + warnings.warn("writing non-standard TIFF (samplesperpixel 2)") + + if volume and (data.shape[-2] % 16 or data.shape[-3] % 16): + warnings.warn("volume width or length are not multiple of 16") + volume = False + data = numpy.swapaxes(data, 1, 2) + data = data.reshape( + (data.shape[0] * data.shape[1],) + data.shape[2:]) + + # data.shape is now normalized 5D or 6D, depending on volume + # (pages, planar_samples, (depth,) height, width, contig_samples) + assert len(data.shape) in (5, 6) + shape = data.shape + + bytestr = bytes if sys.version[0] == '2' else ( + lambda x: bytes(x, 'utf-8') if isinstance(x, str) else x) + tags = [] # list of (code, ifdentry, ifdvalue, writeonce) + + if volume: + # use tiles to save volume data + tag_byte_counts = TiffWriter.TAGS['tile_byte_counts'] + tag_offsets = TiffWriter.TAGS['tile_offsets'] + else: + # else use strips + tag_byte_counts = TiffWriter.TAGS['strip_byte_counts'] + tag_offsets = TiffWriter.TAGS['strip_offsets'] + + def pack(fmt, *val): + return struct.pack(byteorder+fmt, *val) + + def addtag(code, dtype, count, value, writeonce=False): + # Compute ifdentry & ifdvalue bytes from code, dtype, count, value. + # Append (code, ifdentry, ifdvalue, writeonce) to tags list. + code = int(TiffWriter.TAGS.get(code, code)) + try: + tifftype = TiffWriter.TYPES[dtype] + except KeyError: + raise ValueError("unknown dtype %s" % dtype) + rawcount = count + if dtype == 's': + value = bytestr(value) + b'\0' + count = rawcount = len(value) + value = (value, ) + if len(dtype) > 1: + count *= int(dtype[:-1]) + dtype = dtype[-1] + ifdentry = [pack('HH', code, tifftype), + pack(offset_format, rawcount)] + ifdvalue = None + if count == 1: + if isinstance(value, (tuple, list)): + value = value[0] + ifdentry.append(pack(val_format, pack(dtype, value))) + elif struct.calcsize(dtype) * count <= offset_size: + ifdentry.append(pack(val_format, + pack(str(count)+dtype, *value))) + else: + ifdentry.append(pack(offset_format, 0)) + ifdvalue = pack(str(count)+dtype, *value) + tags.append((code, b''.join(ifdentry), ifdvalue, writeonce)) + + def rational(arg, max_denominator=1000000): + # return nominator and denominator from float or two integers + try: + f = Fraction.from_float(arg) + except TypeError: + f = Fraction(arg[0], arg[1]) + f = f.limit_denominator(max_denominator) + return f.numerator, f.denominator + + if self._software: + addtag('software', 's', 0, self._software, writeonce=True) + self._software = None # only save to first page + if description: + addtag('image_description', 's', 0, description, writeonce=True) + elif writeshape and shape[0] > 1 and shape != data_shape: + addtag('image_description', 's', 0, + "shape=(%s)" % (",".join('%i' % i for i in data_shape)), + writeonce=True) + addtag('datetime', 's', 0, + datetime.datetime.now().strftime("%Y:%m:%d %H:%M:%S"), + writeonce=True) + addtag('compression', 'H', 1, 32946 if compress else 1) + addtag('orientation', 'H', 1, 1) + addtag('image_width', 'I', 1, shape[-2]) + addtag('image_length', 'I', 1, shape[-3]) + if volume: + addtag('image_depth', 'I', 1, shape[-4]) + addtag('tile_depth', 'I', 1, shape[-4]) + addtag('tile_width', 'I', 1, shape[-2]) + addtag('tile_length', 'I', 1, shape[-3]) + addtag('new_subfile_type', 'I', 1, 0 if shape[0] == 1 else 2) + addtag('sample_format', 'H', 1, + {'u': 1, 'i': 2, 'f': 3, 'c': 6}[data.dtype.kind]) + addtag('photometric', 'H', 1, + {'miniswhite': 0, 'minisblack': 1, 'rgb': 2}[photometric]) + addtag('samples_per_pixel', 'H', 1, samplesperpixel) + if planarconfig and samplesperpixel > 1: + addtag('planar_configuration', 'H', 1, 1 + if planarconfig == 'contig' else 2) + addtag('bits_per_sample', 'H', samplesperpixel, + (data.dtype.itemsize * 8, ) * samplesperpixel) + else: + addtag('bits_per_sample', 'H', 1, data.dtype.itemsize * 8) + if extrasamples: + if photometric == 'rgb' and extrasamples == 1: + addtag('extra_samples', 'H', 1, 1) # associated alpha channel + else: + addtag('extra_samples', 'H', extrasamples, (0,) * extrasamples) + if resolution: + addtag('x_resolution', '2I', 1, rational(resolution[0])) + addtag('y_resolution', '2I', 1, rational(resolution[1])) + addtag('resolution_unit', 'H', 1, 2) + addtag('rows_per_strip', 'I', 1, + shape[-3] * (shape[-4] if volume else 1)) + + # use one strip or tile per plane + strip_byte_counts = (data[0, 0].size * data.dtype.itemsize,) * shape[1] + addtag(tag_byte_counts, offset_format, shape[1], strip_byte_counts) + addtag(tag_offsets, offset_format, shape[1], (0, ) * shape[1]) + + # add extra tags from users + for t in extratags: + addtag(*t) + # the entries in an IFD must be sorted in ascending order by tag code + tags = sorted(tags, key=lambda x: x[0]) + + if not self._bigtiff and (fh.tell() + data.size*data.dtype.itemsize + > 2**31-1): + raise ValueError("data too large for non-bigtiff file") + + for pageindex in range(shape[0]): + # update pointer at ifd_offset + pos = fh.tell() + fh.seek(self._ifd_offset) + fh.write(pack(offset_format, pos)) + fh.seek(pos) + + # write ifdentries + fh.write(pack(numtag_format, len(tags))) + tag_offset = fh.tell() + fh.write(b''.join(t[1] for t in tags)) + self._ifd_offset = fh.tell() + fh.write(pack(offset_format, 0)) # offset to next IFD + + # write tag values and patch offsets in ifdentries, if necessary + for tagindex, tag in enumerate(tags): + if tag[2]: + pos = fh.tell() + fh.seek(tag_offset + tagindex*tag_size + offset_size + 4) + fh.write(pack(offset_format, pos)) + fh.seek(pos) + if tag[0] == tag_offsets: + strip_offsets_offset = pos + elif tag[0] == tag_byte_counts: + strip_byte_counts_offset = pos + fh.write(tag[2]) + + # write image data + data_offset = fh.tell() + if compress: + strip_byte_counts = [] + for plane in data[pageindex]: + plane = zlib.compress(plane, compress) + strip_byte_counts.append(len(plane)) + fh.write(plane) + else: + # if this fails try update Python/numpy + data[pageindex].tofile(fh) + fh.flush() + + # update strip and tile offsets and byte_counts if necessary + pos = fh.tell() + for tagindex, tag in enumerate(tags): + if tag[0] == tag_offsets: # strip or tile offsets + if tag[2]: + fh.seek(strip_offsets_offset) + strip_offset = data_offset + for size in strip_byte_counts: + fh.write(pack(offset_format, strip_offset)) + strip_offset += size + else: + fh.seek(tag_offset + tagindex*tag_size + + offset_size + 4) + fh.write(pack(offset_format, data_offset)) + elif tag[0] == tag_byte_counts: # strip or tile byte_counts + if compress: + if tag[2]: + fh.seek(strip_byte_counts_offset) + for size in strip_byte_counts: + fh.write(pack(offset_format, size)) + else: + fh.seek(tag_offset + tagindex*tag_size + + offset_size + 4) + fh.write(pack(offset_format, strip_byte_counts[0])) + break + fh.seek(pos) + fh.flush() + # remove tags that should be written only once + if pageindex == 0: + tags = [t for t in tags if not t[-1]] + + def close(self): + self._fh.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + +def imread(files, **kwargs): + """Return image data from TIFF file(s) as numpy array. + + The first image series is returned if no arguments are provided. + + Parameters + ---------- + files : str or list + File name, glob pattern, or list of file names. + key : int, slice, or sequence of page indices + Defines which pages to return as array. + series : int + Defines which series of pages in file to return as array. + multifile : bool + If True (default), OME-TIFF data may include pages from multiple files. + pattern : str + Regular expression pattern that matches axes names and indices in + file names. + kwargs : dict + Additional parameters passed to the TiffFile or TiffSequence asarray + function. + + Examples + -------- + >>> im = imread('test.tif', key=0) + >>> im.shape + (256, 256, 4) + >>> ims = imread(['test.tif', 'test.tif']) + >>> ims.shape + (2, 256, 256, 4) + + """ + kwargs_file = {} + if 'multifile' in kwargs: + kwargs_file['multifile'] = kwargs['multifile'] + del kwargs['multifile'] + else: + kwargs_file['multifile'] = True + kwargs_seq = {} + if 'pattern' in kwargs: + kwargs_seq['pattern'] = kwargs['pattern'] + del kwargs['pattern'] + + if isinstance(files, basestring) and any(i in files for i in '?*'): + files = glob.glob(files) + if not files: + raise ValueError('no files found') + if len(files) == 1: + files = files[0] + + if isinstance(files, basestring): + with TiffFile(files, **kwargs_file) as tif: + return tif.asarray(**kwargs) + else: + with TiffSequence(files, **kwargs_seq) as imseq: + return imseq.asarray(**kwargs) + + +class lazyattr(object): + """Lazy object attribute whose value is computed on first access.""" + __slots__ = ('func', ) + + def __init__(self, func): + self.func = func + + def __get__(self, instance, owner): + if instance is None: + return self + value = self.func(instance) + if value is NotImplemented: + return getattr(super(owner, instance), self.func.__name__) + setattr(instance, self.func.__name__, value) + return value + + +class TiffFile(object): + """Read image and metadata from TIFF, STK, LSM, and FluoView files. + + TiffFile instances must be closed using the close method, which is + automatically called when using the 'with' statement. + + Attributes + ---------- + pages : list + All TIFF pages in file. + series : list of Records(shape, dtype, axes, TiffPages) + TIFF pages with compatible shapes and types. + micromanager_metadata: dict + Extra MicroManager non-TIFF metadata in the file, if exists. + + All attributes are read-only. + + Examples + -------- + >>> with TiffFile('test.tif') as tif: + ... data = tif.asarray() + ... data.shape + (256, 256, 4) + + """ + def __init__(self, arg, name=None, offset=None, size=None, + multifile=True, multifile_close=True): + """Initialize instance from file. + + Parameters + ---------- + arg : str or open file + Name of file or open file object. + The file objects are closed in TiffFile.close(). + name : str + Optional name of file in case 'arg' is a file handle. + offset : int + Optional start position of embedded file. By default this is + the current file position. + size : int + Optional size of embedded file. By default this is the number + of bytes from the 'offset' to the end of the file. + multifile : bool + If True (default), series may include pages from multiple files. + Currently applies to OME-TIFF only. + multifile_close : bool + If True (default), keep the handles of other files in multifile + series closed. This is inefficient when few files refer to + many pages. If False, the C runtime may run out of resources. + + """ + self._fh = FileHandle(arg, name=name, offset=offset, size=size) + self.offset_size = None + self.pages = [] + self._multifile = bool(multifile) + self._multifile_close = bool(multifile_close) + self._files = {self._fh.name: self} # cache of TiffFiles + try: + self._fromfile() + except Exception: + self._fh.close() + raise + + @property + def filehandle(self): + """Return file handle.""" + return self._fh + + @property + def filename(self): + """Return name of file handle.""" + return self._fh.name + + def close(self): + """Close open file handle(s).""" + for tif in self._files.values(): + tif._fh.close() + self._files = {} + + def _fromfile(self): + """Read TIFF header and all page records from file.""" + self._fh.seek(0) + try: + self.byteorder = {b'II': '<', b'MM': '>'}[self._fh.read(2)] + except KeyError: + raise ValueError("not a valid TIFF file") + version = struct.unpack(self.byteorder+'H', self._fh.read(2))[0] + if version == 43: # BigTiff + self.offset_size, zero = struct.unpack(self.byteorder+'HH', + self._fh.read(4)) + if zero or self.offset_size != 8: + raise ValueError("not a valid BigTIFF file") + elif version == 42: + self.offset_size = 4 + else: + raise ValueError("not a TIFF file") + self.pages = [] + while True: + try: + page = TiffPage(self) + self.pages.append(page) + except StopIteration: + break + if not self.pages: + raise ValueError("empty TIFF file") + + if self.is_micromanager: + # MicroManager files contain metadata not stored in TIFF tags. + self.micromanager_metadata = read_micromanager_metadata(self._fh) + + if self.is_lsm: + self._fix_lsm_strip_offsets() + self._fix_lsm_strip_byte_counts() + + def _fix_lsm_strip_offsets(self): + """Unwrap strip offsets for LSM files greater than 4 GB.""" + for series in self.series: + wrap = 0 + previous_offset = 0 + for page in series.pages: + strip_offsets = [] + for current_offset in page.strip_offsets: + if current_offset < previous_offset: + wrap += 2**32 + strip_offsets.append(current_offset + wrap) + previous_offset = current_offset + page.strip_offsets = tuple(strip_offsets) + + def _fix_lsm_strip_byte_counts(self): + """Set strip_byte_counts to size of compressed data. + + The strip_byte_counts tag in LSM files contains the number of bytes + for the uncompressed data. + + """ + if not self.pages: + return + strips = {} + for page in self.pages: + assert len(page.strip_offsets) == len(page.strip_byte_counts) + for offset, bytecount in zip(page.strip_offsets, + page.strip_byte_counts): + strips[offset] = bytecount + offsets = sorted(strips.keys()) + offsets.append(min(offsets[-1] + strips[offsets[-1]], self._fh.size)) + for i, offset in enumerate(offsets[:-1]): + strips[offset] = min(strips[offset], offsets[i+1] - offset) + for page in self.pages: + if page.compression: + page.strip_byte_counts = tuple( + strips[offset] for offset in page.strip_offsets) + + @lazyattr + def series(self): + """Return series of TiffPage with compatible shape and properties.""" + if not self.pages: + return [] + + series = [] + page0 = self.pages[0] + + if self.is_ome: + series = self._omeseries() + elif self.is_fluoview: + dims = {b'X': 'X', b'Y': 'Y', b'Z': 'Z', b'T': 'T', + b'WAVELENGTH': 'C', b'TIME': 'T', b'XY': 'R', + b'EVENT': 'V', b'EXPOSURE': 'L'} + mmhd = list(reversed(page0.mm_header.dimensions)) + series = [Record( + axes=''.join(dims.get(i[0].strip().upper(), 'Q') + for i in mmhd if i[1] > 1), + shape=tuple(int(i[1]) for i in mmhd if i[1] > 1), + pages=self.pages, dtype=numpy.dtype(page0.dtype))] + elif self.is_lsm: + lsmi = page0.cz_lsm_info + axes = CZ_SCAN_TYPES[lsmi.scan_type] + if page0.is_rgb: + axes = axes.replace('C', '').replace('XY', 'XYC') + axes = axes[::-1] + shape = tuple(getattr(lsmi, CZ_DIMENSIONS[i]) for i in axes) + pages = [p for p in self.pages if not p.is_reduced] + series = [Record(axes=axes, shape=shape, pages=pages, + dtype=numpy.dtype(pages[0].dtype))] + if len(pages) != len(self.pages): # reduced RGB pages + pages = [p for p in self.pages if p.is_reduced] + cp = 1 + i = 0 + while cp < len(pages) and i < len(shape)-2: + cp *= shape[i] + i += 1 + shape = shape[:i] + pages[0].shape + axes = axes[:i] + 'CYX' + series.append(Record(axes=axes, shape=shape, pages=pages, + dtype=numpy.dtype(pages[0].dtype))) + elif self.is_imagej: + shape = [] + axes = [] + ij = page0.imagej_tags + if 'frames' in ij: + shape.append(ij['frames']) + axes.append('T') + if 'slices' in ij: + shape.append(ij['slices']) + axes.append('Z') + if 'channels' in ij and not self.is_rgb: + shape.append(ij['channels']) + axes.append('C') + remain = len(self.pages) // (product(shape) if shape else 1) + if remain > 1: + shape.append(remain) + axes.append('I') + shape.extend(page0.shape) + axes.extend(page0.axes) + axes = ''.join(axes) + series = [Record(pages=self.pages, shape=tuple(shape), axes=axes, + dtype=numpy.dtype(page0.dtype))] + elif self.is_nih: + if len(self.pages) == 1: + shape = page0.shape + axes = page0.axes + else: + shape = (len(self.pages),) + page0.shape + axes = 'I' + page0.axes + series = [Record(pages=self.pages, shape=shape, axes=axes, + dtype=numpy.dtype(page0.dtype))] + elif page0.is_shaped: + # TODO: shaped files can contain multiple series + shape = page0.tags['image_description'].value[7:-1] + shape = tuple(int(i) for i in shape.split(b',')) + series = [Record(pages=self.pages, shape=shape, + axes='Q' * len(shape), + dtype=numpy.dtype(page0.dtype))] + + # generic detection of series + if not series: + shapes = [] + pages = {} + for page in self.pages: + if not page.shape: + continue + shape = page.shape + (page.axes, + page.compression in TIFF_DECOMPESSORS) + if shape not in pages: + shapes.append(shape) + pages[shape] = [page] + else: + pages[shape].append(page) + series = [Record(pages=pages[s], + axes=(('I' + s[-2]) + if len(pages[s]) > 1 else s[-2]), + dtype=numpy.dtype(pages[s][0].dtype), + shape=((len(pages[s]), ) + s[:-2] + if len(pages[s]) > 1 else s[:-2])) + for s in shapes] + + # remove empty series, e.g. in MD Gel files + series = [s for s in series if sum(s.shape) > 0] + + return series + + def asarray(self, key=None, series=None, memmap=False): + """Return image data from multiple TIFF pages as numpy array. + + By default the first image series is returned. + + Parameters + ---------- + key : int, slice, or sequence of page indices + Defines which pages to return as array. + series : int + Defines which series of pages to return as array. + memmap : bool + If True, return an array stored in a binary file on disk + if possible. + + """ + if key is None and series is None: + series = 0 + if series is not None: + pages = self.series[series].pages + else: + pages = self.pages + + if key is None: + pass + elif isinstance(key, int): + pages = [pages[key]] + elif isinstance(key, slice): + pages = pages[key] + elif isinstance(key, collections.Iterable): + pages = [pages[k] for k in key] + else: + raise TypeError("key must be an int, slice, or sequence") + + if not len(pages): + raise ValueError("no pages selected") + + if self.is_nih: + if pages[0].is_palette: + result = stack_pages(pages, colormapped=False, squeeze=False) + result = numpy.take(pages[0].color_map, result, axis=1) + result = numpy.swapaxes(result, 0, 1) + else: + result = stack_pages(pages, memmap=memmap, + colormapped=False, squeeze=False) + elif len(pages) == 1: + return pages[0].asarray(memmap=memmap) + elif self.is_ome: + assert not self.is_palette, "color mapping disabled for ome-tiff" + if any(p is None for p in pages): + # zero out missing pages + firstpage = next(p for p in pages if p) + nopage = numpy.zeros_like( + firstpage.asarray(memmap=False)) + s = self.series[series] + if memmap: + with tempfile.NamedTemporaryFile() as fh: + result = numpy.memmap(fh, dtype=s.dtype, shape=s.shape) + result = result.reshape(-1) + else: + result = numpy.empty(s.shape, s.dtype).reshape(-1) + index = 0 + + class KeepOpen: + # keep Tiff files open between consecutive pages + def __init__(self, parent, close): + self.master = parent + self.parent = parent + self._close = close + + def open(self, page): + if self._close and page and page.parent != self.parent: + if self.parent != self.master: + self.parent.filehandle.close() + self.parent = page.parent + self.parent.filehandle.open() + + def close(self): + if self._close and self.parent != self.master: + self.parent.filehandle.close() + + keep = KeepOpen(self, self._multifile_close) + for page in pages: + keep.open(page) + if page: + a = page.asarray(memmap=False, colormapped=False, + reopen=False) + else: + a = nopage + try: + result[index:index + a.size] = a.reshape(-1) + except ValueError as e: + warnings.warn("ome-tiff: %s" % e) + break + index += a.size + keep.close() + else: + result = stack_pages(pages, memmap=memmap) + + if key is None: + try: + result.shape = self.series[series].shape + except ValueError: + try: + warnings.warn("failed to reshape %s to %s" % ( + result.shape, self.series[series].shape)) + # try series of expected shapes + result.shape = (-1,) + self.series[series].shape + except ValueError: + # revert to generic shape + result.shape = (-1,) + pages[0].shape + else: + result.shape = (-1,) + pages[0].shape + return result + + def _omeseries(self): + """Return image series in OME-TIFF file(s).""" + root = etree.fromstring(self.pages[0].tags['image_description'].value) + uuid = root.attrib.get('UUID', None) + self._files = {uuid: self} + dirname = self._fh.dirname + modulo = {} + result = [] + for element in root: + if element.tag.endswith('BinaryOnly'): + warnings.warn("ome-xml: not an ome-tiff master file") + break + if element.tag.endswith('StructuredAnnotations'): + for annot in element: + if not annot.attrib.get('Namespace', + '').endswith('modulo'): + continue + for value in annot: + for modul in value: + for along in modul: + if not along.tag[:-1].endswith('Along'): + continue + axis = along.tag[-1] + newaxis = along.attrib.get('Type', 'other') + newaxis = AXES_LABELS[newaxis] + if 'Start' in along.attrib: + labels = range( + int(along.attrib['Start']), + int(along.attrib['End']) + 1, + int(along.attrib.get('Step', 1))) + else: + labels = [label.text for label in along + if label.tag.endswith('Label')] + modulo[axis] = (newaxis, labels) + if not element.tag.endswith('Image'): + continue + for pixels in element: + if not pixels.tag.endswith('Pixels'): + continue + atr = pixels.attrib + dtype = atr.get('Type', None) + axes = ''.join(reversed(atr['DimensionOrder'])) + shape = list(int(atr['Size'+ax]) for ax in axes) + size = product(shape[:-2]) + ifds = [None] * size + for data in pixels: + if not data.tag.endswith('TiffData'): + continue + atr = data.attrib + ifd = int(atr.get('IFD', 0)) + num = int(atr.get('NumPlanes', 1 if 'IFD' in atr else 0)) + num = int(atr.get('PlaneCount', num)) + idx = [int(atr.get('First'+ax, 0)) for ax in axes[:-2]] + try: + idx = numpy.ravel_multi_index(idx, shape[:-2]) + except ValueError: + # ImageJ produces invalid ome-xml when cropping + warnings.warn("ome-xml: invalid TiffData index") + continue + for uuid in data: + if not uuid.tag.endswith('UUID'): + continue + if uuid.text not in self._files: + if not self._multifile: + # abort reading multifile OME series + # and fall back to generic series + return [] + fname = uuid.attrib['FileName'] + try: + tif = TiffFile(os.path.join(dirname, fname)) + except (IOError, ValueError): + tif.close() + warnings.warn( + "ome-xml: failed to read '%s'" % fname) + break + self._files[uuid.text] = tif + if self._multifile_close: + tif.close() + pages = self._files[uuid.text].pages + try: + for i in range(num if num else len(pages)): + ifds[idx + i] = pages[ifd + i] + except IndexError: + warnings.warn("ome-xml: index out of range") + # only process first uuid + break + else: + pages = self.pages + try: + for i in range(num if num else len(pages)): + ifds[idx + i] = pages[ifd + i] + except IndexError: + warnings.warn("ome-xml: index out of range") + if all(i is None for i in ifds): + # skip images without data + continue + dtype = next(i for i in ifds if i).dtype + result.append(Record(axes=axes, shape=shape, pages=ifds, + dtype=numpy.dtype(dtype))) + + for record in result: + for axis, (newaxis, labels) in modulo.items(): + i = record.axes.index(axis) + size = len(labels) + if record.shape[i] == size: + record.axes = record.axes.replace(axis, newaxis, 1) + else: + record.shape[i] //= size + record.shape.insert(i+1, size) + record.axes = record.axes.replace(axis, axis+newaxis, 1) + record.shape = tuple(record.shape) + + # squeeze dimensions + for record in result: + record.shape, record.axes = squeeze_axes(record.shape, record.axes) + + return result + + def __len__(self): + """Return number of image pages in file.""" + return len(self.pages) + + def __getitem__(self, key): + """Return specified page.""" + return self.pages[key] + + def __iter__(self): + """Return iterator over pages.""" + return iter(self.pages) + + def __str__(self): + """Return string containing information about file.""" + result = [ + self._fh.name.capitalize(), + format_size(self._fh.size), + {'<': 'little endian', '>': 'big endian'}[self.byteorder]] + if self.is_bigtiff: + result.append("bigtiff") + if len(self.pages) > 1: + result.append("%i pages" % len(self.pages)) + if len(self.series) > 1: + result.append("%i series" % len(self.series)) + if len(self._files) > 1: + result.append("%i files" % (len(self._files))) + return ", ".join(result) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + @lazyattr + def fstat(self): + try: + return os.fstat(self._fh.fileno()) + except Exception: # io.UnsupportedOperation + return None + + @lazyattr + def is_bigtiff(self): + return self.offset_size != 4 + + @lazyattr + def is_rgb(self): + return all(p.is_rgb for p in self.pages) + + @lazyattr + def is_palette(self): + return all(p.is_palette for p in self.pages) + + @lazyattr + def is_mdgel(self): + return any(p.is_mdgel for p in self.pages) + + @lazyattr + def is_mediacy(self): + return any(p.is_mediacy for p in self.pages) + + @lazyattr + def is_stk(self): + return all(p.is_stk for p in self.pages) + + @lazyattr + def is_lsm(self): + return self.pages[0].is_lsm + + @lazyattr + def is_imagej(self): + return self.pages[0].is_imagej + + @lazyattr + def is_micromanager(self): + return self.pages[0].is_micromanager + + @lazyattr + def is_nih(self): + return self.pages[0].is_nih + + @lazyattr + def is_fluoview(self): + return self.pages[0].is_fluoview + + @lazyattr + def is_ome(self): + return self.pages[0].is_ome + + +class TiffPage(object): + """A TIFF image file directory (IFD). + + Attributes + ---------- + index : int + Index of page in file. + dtype : str {TIFF_SAMPLE_DTYPES} + Data type of image, colormapped if applicable. + shape : tuple + Dimensions of the image array in TIFF page, + colormapped and with one alpha channel if applicable. + axes : str + Axes label codes: + 'X' width, 'Y' height, 'S' sample, 'I' image series|page|plane, + 'Z' depth, 'C' color|em-wavelength|channel, 'E' ex-wavelength|lambda, + 'T' time, 'R' region|tile, 'A' angle, 'P' phase, 'H' lifetime, + 'L' exposure, 'V' event, 'Q' unknown, '_' missing + tags : TiffTags + Dictionary of tags in page. + Tag values are also directly accessible as attributes. + color_map : numpy array + Color look up table, if exists. + cz_lsm_scan_info: Record(dict) + LSM scan info attributes, if exists. + imagej_tags: Record(dict) + Consolidated ImageJ description and metadata tags, if exists. + uic_tags: Record(dict) + Consolidated MetaMorph STK/UIC tags, if exists. + + All attributes are read-only. + + Notes + ----- + The internal, normalized '_shape' attribute is 6 dimensional: + + 0. number planes (stk) + 1. planar samples_per_pixel + 2. image_depth Z (sgi) + 3. image_length Y + 4. image_width X + 5. contig samples_per_pixel + + """ + def __init__(self, parent): + """Initialize instance from file.""" + self.parent = parent + self.index = len(parent.pages) + self.shape = self._shape = () + self.dtype = self._dtype = None + self.axes = "" + self.tags = TiffTags() + + self._fromfile() + self._process_tags() + + def _fromfile(self): + """Read TIFF IFD structure and its tags from file. + + File cursor must be at storage position of IFD offset and is left at + offset to next IFD. + + Raises StopIteration if offset (first bytes read) is 0. + + """ + fh = self.parent.filehandle + byteorder = self.parent.byteorder + offset_size = self.parent.offset_size + + fmt = {4: 'I', 8: 'Q'}[offset_size] + offset = struct.unpack(byteorder + fmt, fh.read(offset_size))[0] + if not offset: + raise StopIteration() + + # read standard tags + tags = self.tags + fh.seek(offset) + fmt, size = {4: ('H', 2), 8: ('Q', 8)}[offset_size] + try: + numtags = struct.unpack(byteorder + fmt, fh.read(size))[0] + except Exception: + warnings.warn("corrupted page list") + raise StopIteration() + + tagcode = 0 + for _ in range(numtags): + try: + tag = TiffTag(self.parent) + # print(tag) + except TiffTag.Error as e: + warnings.warn(str(e)) + continue + if tagcode > tag.code: + # expected for early LSM and tifffile versions + warnings.warn("tags are not ordered by code") + tagcode = tag.code + if tag.name not in tags: + tags[tag.name] = tag + else: + # some files contain multiple IFD with same code + # e.g. MicroManager files contain two image_description + i = 1 + while True: + name = "%s_%i" % (tag.name, i) + if name not in tags: + tags[name] = tag + break + + pos = fh.tell() + + if self.is_lsm or (self.index and self.parent.is_lsm): + # correct non standard LSM bitspersample tags + self.tags['bits_per_sample']._correct_lsm_bitspersample(self) + + if self.is_lsm: + # read LSM info subrecords + for name, reader in CZ_LSM_INFO_READERS.items(): + try: + offset = self.cz_lsm_info['offset_'+name] + except KeyError: + continue + if offset < 8: + # older LSM revision + continue + fh.seek(offset) + try: + setattr(self, 'cz_lsm_'+name, reader(fh)) + except ValueError: + pass + + elif self.is_stk and 'uic1tag' in tags and not tags['uic1tag'].value: + # read uic1tag now that plane count is known + uic1tag = tags['uic1tag'] + fh.seek(uic1tag.value_offset) + tags['uic1tag'].value = Record( + read_uic1tag(fh, byteorder, uic1tag.dtype, uic1tag.count, + tags['uic2tag'].count)) + fh.seek(pos) + + def _process_tags(self): + """Validate standard tags and initialize attributes. + + Raise ValueError if tag values are not supported. + + """ + tags = self.tags + for code, (name, default, dtype, count, validate) in TIFF_TAGS.items(): + if not (name in tags or default is None): + tags[name] = TiffTag(code, dtype=dtype, count=count, + value=default, name=name) + if name in tags and validate: + try: + if tags[name].count == 1: + setattr(self, name, validate[tags[name].value]) + else: + setattr(self, name, tuple( + validate[value] for value in tags[name].value)) + except KeyError: + raise ValueError("%s.value (%s) not supported" % + (name, tags[name].value)) + + tag = tags['bits_per_sample'] + if tag.count == 1: + self.bits_per_sample = tag.value + else: + # LSM might list more items than samples_per_pixel + value = tag.value[:self.samples_per_pixel] + if any((v-value[0] for v in value)): + self.bits_per_sample = value + else: + self.bits_per_sample = value[0] + + tag = tags['sample_format'] + if tag.count == 1: + self.sample_format = TIFF_SAMPLE_FORMATS[tag.value] + else: + value = tag.value[:self.samples_per_pixel] + if any((v-value[0] for v in value)): + self.sample_format = [TIFF_SAMPLE_FORMATS[v] for v in value] + else: + self.sample_format = TIFF_SAMPLE_FORMATS[value[0]] + + if 'photometric' not in tags: + self.photometric = None + + if 'image_depth' not in tags: + self.image_depth = 1 + + if 'image_length' in tags: + self.strips_per_image = int(math.floor( + float(self.image_length + self.rows_per_strip - 1) / + self.rows_per_strip)) + else: + self.strips_per_image = 0 + + key = (self.sample_format, self.bits_per_sample) + self.dtype = self._dtype = TIFF_SAMPLE_DTYPES.get(key, None) + + if 'image_length' not in self.tags or 'image_width' not in self.tags: + # some GEL file pages are missing image data + self.image_length = 0 + self.image_width = 0 + self.image_depth = 0 + self.strip_offsets = 0 + self._shape = () + self.shape = () + self.axes = '' + + if self.is_palette: + self.dtype = self.tags['color_map'].dtype[1] + self.color_map = numpy.array(self.color_map, self.dtype) + dmax = self.color_map.max() + if dmax < 256: + self.dtype = numpy.uint8 + self.color_map = self.color_map.astype(self.dtype) + #else: + # self.dtype = numpy.uint8 + # self.color_map >>= 8 + # self.color_map = self.color_map.astype(self.dtype) + self.color_map.shape = (3, -1) + + # determine shape of data + image_length = self.image_length + image_width = self.image_width + image_depth = self.image_depth + samples_per_pixel = self.samples_per_pixel + + if self.is_stk: + assert self.image_depth == 1 + planes = self.tags['uic2tag'].count + if self.is_contig: + self._shape = (planes, 1, 1, image_length, image_width, + samples_per_pixel) + if samples_per_pixel == 1: + self.shape = (planes, image_length, image_width) + self.axes = 'YX' + else: + self.shape = (planes, image_length, image_width, + samples_per_pixel) + self.axes = 'YXS' + else: + self._shape = (planes, samples_per_pixel, 1, image_length, + image_width, 1) + if samples_per_pixel == 1: + self.shape = (planes, image_length, image_width) + self.axes = 'YX' + else: + self.shape = (planes, samples_per_pixel, image_length, + image_width) + self.axes = 'SYX' + # detect type of series + if planes == 1: + self.shape = self.shape[1:] + elif numpy.all(self.uic2tag.z_distance != 0): + self.axes = 'Z' + self.axes + elif numpy.all(numpy.diff(self.uic2tag.time_created) != 0): + self.axes = 'T' + self.axes + else: + self.axes = 'I' + self.axes + # DISABLED + if self.is_palette: + assert False, "color mapping disabled for stk" + if self.color_map.shape[1] >= 2**self.bits_per_sample: + if image_depth == 1: + self.shape = (3, planes, image_length, image_width) + else: + self.shape = (3, planes, image_depth, image_length, + image_width) + self.axes = 'C' + self.axes + else: + warnings.warn("palette cannot be applied") + self.is_palette = False + elif self.is_palette: + samples = 1 + if 'extra_samples' in self.tags: + samples += len(self.extra_samples) + if self.is_contig: + self._shape = (1, 1, image_depth, image_length, image_width, + samples) + else: + self._shape = (1, samples, image_depth, image_length, + image_width, 1) + if self.color_map.shape[1] >= 2**self.bits_per_sample: + if image_depth == 1: + self.shape = (3, image_length, image_width) + self.axes = 'CYX' + else: + self.shape = (3, image_depth, image_length, image_width) + self.axes = 'CZYX' + else: + warnings.warn("palette cannot be applied") + self.is_palette = False + if image_depth == 1: + self.shape = (image_length, image_width) + self.axes = 'YX' + else: + self.shape = (image_depth, image_length, image_width) + self.axes = 'ZYX' + elif self.is_rgb or samples_per_pixel > 1: + if self.is_contig: + self._shape = (1, 1, image_depth, image_length, image_width, + samples_per_pixel) + if image_depth == 1: + self.shape = (image_length, image_width, samples_per_pixel) + self.axes = 'YXS' + else: + self.shape = (image_depth, image_length, image_width, + samples_per_pixel) + self.axes = 'ZYXS' + else: + self._shape = (1, samples_per_pixel, image_depth, + image_length, image_width, 1) + if image_depth == 1: + self.shape = (samples_per_pixel, image_length, image_width) + self.axes = 'SYX' + else: + self.shape = (samples_per_pixel, image_depth, + image_length, image_width) + self.axes = 'SZYX' + if False and self.is_rgb and 'extra_samples' in self.tags: + # DISABLED: only use RGB and first alpha channel if exists + extra_samples = self.extra_samples + if self.tags['extra_samples'].count == 1: + extra_samples = (extra_samples, ) + for exs in extra_samples: + if exs in ('unassalpha', 'assocalpha', 'unspecified'): + if self.is_contig: + self.shape = self.shape[:-1] + (4,) + else: + self.shape = (4,) + self.shape[1:] + break + else: + self._shape = (1, 1, image_depth, image_length, image_width, 1) + if image_depth == 1: + self.shape = (image_length, image_width) + self.axes = 'YX' + else: + self.shape = (image_depth, image_length, image_width) + self.axes = 'ZYX' + if not self.compression and 'strip_byte_counts' not in tags: + self.strip_byte_counts = ( + product(self.shape) * (self.bits_per_sample // 8), ) + + assert len(self.shape) == len(self.axes) + + def asarray(self, squeeze=True, colormapped=True, rgbonly=False, + scale_mdgel=False, memmap=False, reopen=True): + """Read image data from file and return as numpy array. + + Raise ValueError if format is unsupported. + If any of 'squeeze', 'colormapped', or 'rgbonly' are not the default, + the shape of the returned array might be different from the page shape. + + Parameters + ---------- + squeeze : bool + If True, all length-1 dimensions (except X and Y) are + squeezed out from result. + colormapped : bool + If True, color mapping is applied for palette-indexed images. + rgbonly : bool + If True, return RGB(A) image without additional extra samples. + memmap : bool + If True, use numpy.memmap to read arrays from file if possible. + For use on 64 bit systems and files with few huge contiguous data. + reopen : bool + If True and the parent file handle is closed, the file is + temporarily re-opened (and closed if no exception occurs). + scale_mdgel : bool + If True, MD Gel data will be scaled according to the private + metadata in the second TIFF page. The dtype will be float32. + + """ + if not self._shape: + return + + if self.dtype is None: + raise ValueError("data type not supported: %s%i" % ( + self.sample_format, self.bits_per_sample)) + if self.compression not in TIFF_DECOMPESSORS: + raise ValueError("cannot decompress %s" % self.compression) + tag = self.tags['sample_format'] + if tag.count != 1 and any((i-tag.value[0] for i in tag.value)): + raise ValueError("sample formats don't match %s" % str(tag.value)) + + fh = self.parent.filehandle + closed = fh.closed + if closed: + if reopen: + fh.open() + else: + raise IOError("file handle is closed") + + dtype = self._dtype + shape = self._shape + image_width = self.image_width + image_length = self.image_length + image_depth = self.image_depth + typecode = self.parent.byteorder + dtype + bits_per_sample = self.bits_per_sample + + if self.is_tiled: + if 'tile_offsets' in self.tags: + byte_counts = self.tile_byte_counts + offsets = self.tile_offsets + else: + byte_counts = self.strip_byte_counts + offsets = self.strip_offsets + tile_width = self.tile_width + tile_length = self.tile_length + tile_depth = self.tile_depth if 'tile_depth' in self.tags else 1 + tw = (image_width + tile_width - 1) // tile_width + tl = (image_length + tile_length - 1) // tile_length + td = (image_depth + tile_depth - 1) // tile_depth + shape = (shape[0], shape[1], + td*tile_depth, tl*tile_length, tw*tile_width, shape[-1]) + tile_shape = (tile_depth, tile_length, tile_width, shape[-1]) + runlen = tile_width + else: + byte_counts = self.strip_byte_counts + offsets = self.strip_offsets + runlen = image_width + + if any(o < 2 for o in offsets): + raise ValueError("corrupted page") + + if memmap and self._is_memmappable(rgbonly, colormapped): + result = fh.memmap_array(typecode, shape, offset=offsets[0]) + elif self.is_contiguous: + fh.seek(offsets[0]) + result = fh.read_array(typecode, product(shape)) + result = result.astype('=' + dtype) + else: + if self.is_contig: + runlen *= self.samples_per_pixel + if bits_per_sample in (8, 16, 32, 64, 128): + if (bits_per_sample * runlen) % 8: + raise ValueError("data and sample size mismatch") + + def unpack(x): + try: + return numpy.fromstring(x, typecode) + except ValueError as e: + # strips may be missing EOI + warnings.warn("unpack: %s" % e) + xlen = ((len(x) // (bits_per_sample // 8)) + * (bits_per_sample // 8)) + return numpy.fromstring(x[:xlen], typecode) + + elif isinstance(bits_per_sample, tuple): + def unpack(x): + return unpackrgb(x, typecode, bits_per_sample) + else: + def unpack(x): + return unpackints(x, typecode, bits_per_sample, runlen) + + decompress = TIFF_DECOMPESSORS[self.compression] + if self.compression == 'jpeg': + table = self.jpeg_tables if 'jpeg_tables' in self.tags else b'' + decompress = lambda x: decodejpg(x, table, self.photometric) + + if self.is_tiled: + result = numpy.empty(shape, dtype) + tw, tl, td, pl = 0, 0, 0, 0 + for offset, bytecount in zip(offsets, byte_counts): + fh.seek(offset) + tile = unpack(decompress(fh.read(bytecount))) + tile.shape = tile_shape + if self.predictor == 'horizontal': + numpy.cumsum(tile, axis=-2, dtype=dtype, out=tile) + result[0, pl, td:td+tile_depth, + tl:tl+tile_length, tw:tw+tile_width, :] = tile + del tile + tw += tile_width + if tw >= shape[4]: + tw, tl = 0, tl + tile_length + if tl >= shape[3]: + tl, td = 0, td + tile_depth + if td >= shape[2]: + td, pl = 0, pl + 1 + result = result[..., + :image_depth, :image_length, :image_width, :] + else: + strip_size = (self.rows_per_strip * self.image_width * + self.samples_per_pixel) + result = numpy.empty(shape, dtype).reshape(-1) + index = 0 + for offset, bytecount in zip(offsets, byte_counts): + fh.seek(offset) + strip = fh.read(bytecount) + strip = decompress(strip) + strip = unpack(strip) + size = min(result.size, strip.size, strip_size, + result.size - index) + result[index:index+size] = strip[:size] + del strip + index += size + + result.shape = self._shape + + if self.predictor == 'horizontal' and not (self.is_tiled and not + self.is_contiguous): + # work around bug in LSM510 software + if not (self.parent.is_lsm and not self.compression): + numpy.cumsum(result, axis=-2, dtype=dtype, out=result) + + if colormapped and self.is_palette: + if self.color_map.shape[1] >= 2**bits_per_sample: + # FluoView and LSM might fail here + result = numpy.take(self.color_map, + result[:, 0, :, :, :, 0], axis=1) + elif rgbonly and self.is_rgb and 'extra_samples' in self.tags: + # return only RGB and first alpha channel if exists + extra_samples = self.extra_samples + if self.tags['extra_samples'].count == 1: + extra_samples = (extra_samples, ) + for i, exs in enumerate(extra_samples): + if exs in ('unassalpha', 'assocalpha', 'unspecified'): + if self.is_contig: + result = result[..., [0, 1, 2, 3+i]] + else: + result = result[:, [0, 1, 2, 3+i]] + break + else: + if self.is_contig: + result = result[..., :3] + else: + result = result[:, :3] + + if squeeze: + try: + result.shape = self.shape + except ValueError: + warnings.warn("failed to reshape from %s to %s" % ( + str(result.shape), str(self.shape))) + + if scale_mdgel and self.parent.is_mdgel: + # MD Gel stores private metadata in the second page + tags = self.parent.pages[1] + if tags.md_file_tag in (2, 128): + scale = tags.md_scale_pixel + scale = scale[0] / scale[1] # rational + result = result.astype('float32') + if tags.md_file_tag == 2: + result **= 2 # squary root data format + result *= scale + + if closed: + # TODO: file remains open if an exception occurred above + fh.close() + return result + + def _is_memmappable(self, rgbonly, colormapped): + """Return if image data in file can be memory mapped.""" + if not self.parent.filehandle.is_file or not self.is_contiguous: + return False + return not (self.predictor or + (rgbonly and 'extra_samples' in self.tags) or + (colormapped and self.is_palette) or + ({'big': '>', 'little': '<'}[sys.byteorder] != + self.parent.byteorder)) + + @lazyattr + def is_contiguous(self): + """Return offset and size of contiguous data, else None. + + Excludes prediction and colormapping. + + """ + if self.compression or self.bits_per_sample not in (8, 16, 32, 64): + return + if self.is_tiled: + if (self.image_width != self.tile_width or + self.image_length % self.tile_length or + self.tile_width % 16 or self.tile_length % 16): + return + if ('image_depth' in self.tags and 'tile_depth' in self.tags and + (self.image_length != self.tile_length or + self.image_depth % self.tile_depth)): + return + offsets = self.tile_offsets + byte_counts = self.tile_byte_counts + else: + offsets = self.strip_offsets + byte_counts = self.strip_byte_counts + if len(offsets) == 1: + return offsets[0], byte_counts[0] + if self.is_stk or all(offsets[i] + byte_counts[i] == offsets[i+1] + or byte_counts[i+1] == 0 # no data/ignore offset + for i in range(len(offsets)-1)): + return offsets[0], sum(byte_counts) + + def __str__(self): + """Return string containing information about page.""" + s = ', '.join(s for s in ( + ' x '.join(str(i) for i in self.shape), + str(numpy.dtype(self.dtype)), + '%s bit' % str(self.bits_per_sample), + self.photometric if 'photometric' in self.tags else '', + self.compression if self.compression else 'raw', + '|'.join(t[3:] for t in ( + 'is_stk', 'is_lsm', 'is_nih', 'is_ome', 'is_imagej', + 'is_micromanager', 'is_fluoview', 'is_mdgel', 'is_mediacy', + 'is_sgi', 'is_reduced', 'is_tiled', + 'is_contiguous') if getattr(self, t))) if s) + return "Page %i: %s" % (self.index, s) + + def __getattr__(self, name): + """Return tag value.""" + if name in self.tags: + value = self.tags[name].value + setattr(self, name, value) + return value + raise AttributeError(name) + + @lazyattr + def uic_tags(self): + """Consolidate UIC tags.""" + if not self.is_stk: + raise AttributeError("uic_tags") + tags = self.tags + result = Record() + result.number_planes = tags['uic2tag'].count + if 'image_description' in tags: + result.plane_descriptions = self.image_description.split(b'\x00') + if 'uic1tag' in tags: + result.update(tags['uic1tag'].value) + if 'uic3tag' in tags: + result.update(tags['uic3tag'].value) # wavelengths + if 'uic4tag' in tags: + result.update(tags['uic4tag'].value) # override uic1 tags + uic2tag = tags['uic2tag'].value + result.z_distance = uic2tag.z_distance + result.time_created = uic2tag.time_created + result.time_modified = uic2tag.time_modified + try: + result.datetime_created = [ + julian_datetime(*dt) for dt in + zip(uic2tag.date_created, uic2tag.time_created)] + result.datetime_modified = [ + julian_datetime(*dt) for dt in + zip(uic2tag.date_modified, uic2tag.time_modified)] + except ValueError as e: + warnings.warn("uic_tags: %s" % e) + return result + + @lazyattr + def imagej_tags(self): + """Consolidate ImageJ metadata.""" + if not self.is_imagej: + raise AttributeError("imagej_tags") + tags = self.tags + if 'image_description_1' in tags: + # MicroManager + result = imagej_description(tags['image_description_1'].value) + else: + result = imagej_description(tags['image_description'].value) + if 'imagej_metadata' in tags: + try: + result.update(imagej_metadata( + tags['imagej_metadata'].value, + tags['imagej_byte_counts'].value, + self.parent.byteorder)) + except Exception as e: + warnings.warn(str(e)) + return Record(result) + + @lazyattr + def is_rgb(self): + """True if page contains a RGB image.""" + return ('photometric' in self.tags and + self.tags['photometric'].value == 2) + + @lazyattr + def is_contig(self): + """True if page contains a contiguous image.""" + return ('planar_configuration' in self.tags and + self.tags['planar_configuration'].value == 1) + + @lazyattr + def is_palette(self): + """True if page contains a palette-colored image and not OME or STK.""" + try: + # turn off color mapping for OME-TIFF and STK + if self.is_stk or self.is_ome or self.parent.is_ome: + return False + except IndexError: + pass # OME-XML not found in first page + return ('photometric' in self.tags and + self.tags['photometric'].value == 3) + + @lazyattr + def is_tiled(self): + """True if page contains tiled image.""" + return 'tile_width' in self.tags + + @lazyattr + def is_reduced(self): + """True if page is a reduced image of another image.""" + return bool(self.tags['new_subfile_type'].value & 1) + + @lazyattr + def is_mdgel(self): + """True if page contains md_file_tag tag.""" + return 'md_file_tag' in self.tags + + @lazyattr + def is_mediacy(self): + """True if page contains Media Cybernetics Id tag.""" + return ('mc_id' in self.tags and + self.tags['mc_id'].value.startswith(b'MC TIFF')) + + @lazyattr + def is_stk(self): + """True if page contains UIC2Tag tag.""" + return 'uic2tag' in self.tags + + @lazyattr + def is_lsm(self): + """True if page contains LSM CZ_LSM_INFO tag.""" + return 'cz_lsm_info' in self.tags + + @lazyattr + def is_fluoview(self): + """True if page contains FluoView MM_STAMP tag.""" + return 'mm_stamp' in self.tags + + @lazyattr + def is_nih(self): + """True if page contains NIH image header.""" + return 'nih_image_header' in self.tags + + @lazyattr + def is_sgi(self): + """True if page contains SGI image and tile depth tags.""" + return 'image_depth' in self.tags and 'tile_depth' in self.tags + + @lazyattr + def is_ome(self): + """True if page contains OME-XML in image_description tag.""" + return ('image_description' in self.tags and self.tags[ + 'image_description'].value.startswith(b' parent.offset_size or code in CUSTOM_TAGS: + pos = fh.tell() + tof = {4: 'I', 8: 'Q'}[parent.offset_size] + self.value_offset = offset = struct.unpack(byteorder+tof, value)[0] + if offset < 0 or offset > parent.filehandle.size: + raise TiffTag.Error("corrupt file - invalid tag value offset") + elif offset < 4: + raise TiffTag.Error("corrupt value offset for tag %i" % code) + fh.seek(offset) + if code in CUSTOM_TAGS: + readfunc = CUSTOM_TAGS[code][1] + value = readfunc(fh, byteorder, dtype, count) + if isinstance(value, dict): # numpy.core.records.record + value = Record(value) + elif code in TIFF_TAGS or dtype[-1] == 's': + value = struct.unpack(fmt, fh.read(size)) + else: + value = read_numpy(fh, byteorder, dtype, count) + fh.seek(pos) + else: + value = struct.unpack(fmt, value[:size]) + + if code not in CUSTOM_TAGS and code not in (273, 279, 324, 325): + # scalar value if not strip/tile offsets/byte_counts + if len(value) == 1: + value = value[0] + + if (dtype.endswith('s') and isinstance(value, bytes) + and self._type != 7): + # TIFF ASCII fields can contain multiple strings, + # each terminated with a NUL + value = stripascii(value) + + self.code = code + self.name = name + self.dtype = dtype + self.count = count + self.value = value + + def _correct_lsm_bitspersample(self, parent): + """Correct LSM bitspersample tag. + + Old LSM writers may use a separate region for two 16-bit values, + although they fit into the tag value element of the tag. + + """ + if self.code == 258 and self.count == 2: + # TODO: test this. Need example file. + warnings.warn("correcting LSM bitspersample tag") + fh = parent.filehandle + tof = {4: '') + + def __str__(self): + """Return string containing information about tag.""" + return ' '.join(str(getattr(self, s)) for s in self.__slots__) + + +class TiffSequence(object): + """Sequence of image files. + + The data shape and dtype of all files must match. + + Properties + ---------- + files : list + List of file names. + shape : tuple + Shape of image sequence. + axes : str + Labels of axes in shape. + + Examples + -------- + >>> tifs = TiffSequence("test.oif.files/*.tif") + >>> tifs.shape, tifs.axes + ((2, 100), 'CT') + >>> data = tifs.asarray() + >>> data.shape + (2, 100, 256, 256) + + """ + _patterns = { + 'axes': r""" + # matches Olympus OIF and Leica TIFF series + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4})) + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + """} + + class ParseError(Exception): + pass + + def __init__(self, files, imread=TiffFile, pattern='axes', + *args, **kwargs): + """Initialize instance from multiple files. + + Parameters + ---------- + files : str, or sequence of str + Glob pattern or sequence of file names. + imread : function or class + Image read function or class with asarray function returning numpy + array from single file. + pattern : str + Regular expression pattern that matches axes names and sequence + indices in file names. + By default this matches Olympus OIF and Leica TIFF series. + + """ + if isinstance(files, basestring): + files = natural_sorted(glob.glob(files)) + files = list(files) + if not files: + raise ValueError("no files found") + #if not os.path.isfile(files[0]): + # raise ValueError("file not found") + self.files = files + + if hasattr(imread, 'asarray'): + # redefine imread + _imread = imread + + def imread(fname, *args, **kwargs): + with _imread(fname) as im: + return im.asarray(*args, **kwargs) + + self.imread = imread + + self.pattern = self._patterns.get(pattern, pattern) + try: + self._parse() + if not self.axes: + self.axes = 'I' + except self.ParseError: + self.axes = 'I' + self.shape = (len(files),) + self._start_index = (0,) + self._indices = tuple((i,) for i in range(len(files))) + + def __str__(self): + """Return string with information about image sequence.""" + return "\n".join([ + self.files[0], + '* files: %i' % len(self.files), + '* axes: %s' % self.axes, + '* shape: %s' % str(self.shape)]) + + def __len__(self): + return len(self.files) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def close(self): + pass + + def asarray(self, memmap=False, *args, **kwargs): + """Read image data from all files and return as single numpy array. + + If memmap is True, return an array stored in a binary file on disk. + The args and kwargs parameters are passed to the imread function. + + Raise IndexError or ValueError if image shapes don't match. + + """ + im = self.imread(self.files[0], *args, **kwargs) + shape = self.shape + im.shape + if memmap: + with tempfile.NamedTemporaryFile() as fh: + result = numpy.memmap(fh, dtype=im.dtype, shape=shape) + else: + result = numpy.zeros(shape, dtype=im.dtype) + result = result.reshape(-1, *im.shape) + for index, fname in zip(self._indices, self.files): + index = [i-j for i, j in zip(index, self._start_index)] + index = numpy.ravel_multi_index(index, self.shape) + im = self.imread(fname, *args, **kwargs) + result[index] = im + result.shape = shape + return result + + def _parse(self): + """Get axes and shape from file names.""" + if not self.pattern: + raise self.ParseError("invalid pattern") + pattern = re.compile(self.pattern, re.IGNORECASE | re.VERBOSE) + matches = pattern.findall(self.files[0]) + if not matches: + raise self.ParseError("pattern doesn't match file names") + matches = matches[-1] + if len(matches) % 2: + raise self.ParseError("pattern doesn't match axis name and index") + axes = ''.join(m for m in matches[::2] if m) + if not axes: + raise self.ParseError("pattern doesn't match file names") + + indices = [] + for fname in self.files: + matches = pattern.findall(fname)[-1] + if axes != ''.join(m for m in matches[::2] if m): + raise ValueError("axes don't match within the image sequence") + indices.append([int(m) for m in matches[1::2] if m]) + shape = tuple(numpy.max(indices, axis=0)) + start_index = tuple(numpy.min(indices, axis=0)) + shape = tuple(i-j+1 for i, j in zip(shape, start_index)) + if product(shape) != len(self.files): + warnings.warn("files are missing. Missing data are zeroed") + + self.axes = axes.upper() + self.shape = shape + self._indices = indices + self._start_index = start_index + + +class Record(dict): + """Dictionary with attribute access. + + Can also be initialized with numpy.core.records.record. + + """ + __slots__ = () + + def __init__(self, arg=None, **kwargs): + if kwargs: + arg = kwargs + elif arg is None: + arg = {} + try: + dict.__init__(self, arg) + except (TypeError, ValueError): + for i, name in enumerate(arg.dtype.names): + v = arg[i] + self[name] = v if v.dtype.char != 'S' else stripnull(v) + + def __getattr__(self, name): + return self[name] + + def __setattr__(self, name, value): + self.__setitem__(name, value) + + def __str__(self): + """Pretty print Record.""" + s = [] + lists = [] + for k in sorted(self): + try: + if k.startswith('_'): # does not work with byte + continue + except AttributeError: + pass + v = self[k] + if isinstance(v, (list, tuple)) and len(v): + if isinstance(v[0], Record): + lists.append((k, v)) + continue + elif isinstance(v[0], TiffPage): + v = [i.index for i in v if i] + s.append( + ("* %s: %s" % (k, str(v))).split("\n", 1)[0] + [:PRINT_LINE_LEN].rstrip()) + for k, v in lists: + l = [] + for i, w in enumerate(v): + l.append("* %s[%i]\n %s" % (k, i, + str(w).replace("\n", "\n "))) + s.append('\n'.join(l)) + return '\n'.join(s) + + +class TiffTags(Record): + """Dictionary of TiffTag with attribute access.""" + + def __str__(self): + """Return string with information about all tags.""" + s = [] + for tag in sorted(self.values(), key=lambda x: x.code): + typecode = "%i%s" % (tag.count * int(tag.dtype[0]), tag.dtype[1]) + line = "* %i %s (%s) %s" % ( + tag.code, tag.name, typecode, tag.as_str()) + s.append(line[:PRINT_LINE_LEN].lstrip()) + return '\n'.join(s) + + +class FileHandle(object): + """Binary file handle. + + * Handle embedded files (for CZI within CZI files). + * Allow to re-open closed files (for multi file formats such as OME-TIFF). + * Read numpy arrays and records from file like objects. + + Only binary read, seek, tell, and close are supported on embedded files. + When initialized from another file handle, do not use it unless this + FileHandle is closed. + + Attributes + ---------- + name : str + Name of the file. + path : str + Absolute path to file. + size : int + Size of file in bytes. + is_file : bool + If True, file has a filno and can be memory mapped. + + All attributes are read-only. + + """ + __slots__ = ('_fh', '_arg', '_mode', '_name', '_dir', + '_offset', '_size', '_close', 'is_file') + + def __init__(self, arg, mode='rb', name=None, offset=None, size=None): + """Initialize file handle from file name or another file handle. + + Parameters + ---------- + arg : str, File, or FileHandle + File name or open file handle. + mode : str + File open mode in case 'arg' is a file name. + name : str + Optional name of file in case 'arg' is a file handle. + offset : int + Optional start position of embedded file. By default this is + the current file position. + size : int + Optional size of embedded file. By default this is the number + of bytes from the 'offset' to the end of the file. + + """ + self._fh = None + self._arg = arg + self._mode = mode + self._name = name + self._dir = '' + self._offset = offset + self._size = size + self._close = True + self.is_file = False + self.open() + + def open(self): + """Open or re-open file.""" + if self._fh: + return # file is open + + if isinstance(self._arg, basestring): + # file name + self._arg = os.path.abspath(self._arg) + self._dir, self._name = os.path.split(self._arg) + self._fh = open(self._arg, self._mode) + self._close = True + if self._offset is None: + self._offset = 0 + elif isinstance(self._arg, FileHandle): + # FileHandle + self._fh = self._arg._fh + if self._offset is None: + self._offset = 0 + self._offset += self._arg._offset + self._close = False + if not self._name: + if self._offset: + name, ext = os.path.splitext(self._arg._name) + self._name = "%s@%i%s" % (name, self._offset, ext) + else: + self._name = self._arg._name + self._dir = self._arg._dir + else: + # open file object + self._fh = self._arg + if self._offset is None: + self._offset = self._arg.tell() + self._close = False + if not self._name: + try: + self._dir, self._name = os.path.split(self._fh.name) + except AttributeError: + self._name = "Unnamed stream" + + if self._offset: + self._fh.seek(self._offset) + + if self._size is None: + pos = self._fh.tell() + self._fh.seek(self._offset, 2) + self._size = self._fh.tell() + self._fh.seek(pos) + + try: + self._fh.fileno() + self.is_file = True + except Exception: + self.is_file = False + + def read(self, size=-1): + """Read 'size' bytes from file, or until EOF is reached.""" + if size < 0 and self._offset: + size = self._size + return self._fh.read(size) + + def memmap_array(self, dtype, shape, offset=0, mode='r', order='C'): + """Return numpy.memmap of data stored in file.""" + if not self.is_file: + raise ValueError("Can not memory map file without fileno.") + return numpy.memmap(self._fh, dtype=dtype, mode=mode, + offset=self._offset + offset, + shape=shape, order=order) + + def read_array(self, dtype, count=-1, sep=""): + """Return numpy array from file. + + Work around numpy issue #2230, "numpy.fromfile does not accept + StringIO object" https://github.com/numpy/numpy/issues/2230. + + """ + try: + return numpy.fromfile(self._fh, dtype, count, sep) + except IOError: + if count < 0: + size = self._size + else: + size = count * numpy.dtype(dtype).itemsize + data = self._fh.read(size) + return numpy.fromstring(data, dtype, count, sep) + + def read_record(self, dtype, shape=1, byteorder=None): + """Return numpy record from file.""" + try: + rec = numpy.rec.fromfile(self._fh, dtype, shape, + byteorder=byteorder) + except Exception: + dtype = numpy.dtype(dtype) + if shape is None: + shape = self._size // dtype.itemsize + size = product(sequence(shape)) * dtype.itemsize + data = self._fh.read(size) + return numpy.rec.fromstring(data, dtype, shape, + byteorder=byteorder) + return rec[0] if shape == 1 else rec + + def tell(self): + """Return file's current position.""" + return self._fh.tell() - self._offset + + def seek(self, offset, whence=0): + """Set file's current position.""" + if self._offset: + if whence == 0: + self._fh.seek(self._offset + offset, whence) + return + elif whence == 2: + self._fh.seek(self._offset + self._size + offset, 0) + return + self._fh.seek(offset, whence) + + def close(self): + """Close file.""" + if self._close and self._fh: + self._fh.close() + self._fh = None + self.is_file = False + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def __getattr__(self, name): + """Return attribute from underlying file object.""" + if self._offset: + warnings.warn( + "FileHandle: '%s' not implemented for embedded files" % name) + return getattr(self._fh, name) + + @property + def name(self): + return self._name + + @property + def dirname(self): + return self._dir + + @property + def path(self): + return os.path.join(self._dir, self._name) + + @property + def size(self): + return self._size + + @property + def closed(self): + return self._fh is None + + +def read_bytes(fh, byteorder, dtype, count): + """Read tag data from file and return as byte string.""" + dtype = 'b' if dtype[-1] == 's' else byteorder+dtype[-1] + return fh.read_array(dtype, count).tostring() + + +def read_numpy(fh, byteorder, dtype, count): + """Read tag data from file and return as numpy array.""" + dtype = 'b' if dtype[-1] == 's' else byteorder+dtype[-1] + return fh.read_array(dtype, count) + + +def read_json(fh, byteorder, dtype, count): + """Read JSON tag data from file and return as object.""" + data = fh.read(count) + try: + return json.loads(unicode(stripnull(data), 'utf-8')) + except ValueError: + warnings.warn("invalid JSON `%s`" % data) + + +def read_mm_header(fh, byteorder, dtype, count): + """Read MM_HEADER tag from file and return as numpy.rec.array.""" + return fh.read_record(MM_HEADER, byteorder=byteorder) + + +def read_mm_stamp(fh, byteorder, dtype, count): + """Read MM_STAMP tag from file and return as numpy.array.""" + return fh.read_array(byteorder+'f8', 8) + + +def read_uic1tag(fh, byteorder, dtype, count, plane_count=None): + """Read MetaMorph STK UIC1Tag from file and return as dictionary. + + Return empty dictionary if plane_count is unknown. + + """ + assert dtype in ('2I', '1I') and byteorder == '<' + result = {} + if dtype == '2I': + # pre MetaMorph 2.5 (not tested) + values = fh.read_array(' structure_size: + break + cz_lsm_info.append((name, dtype)) + else: + cz_lsm_info = CZ_LSM_INFO + + return fh.read_record(cz_lsm_info, byteorder=byteorder) + + +def read_cz_lsm_floatpairs(fh): + """Read LSM sequence of float pairs from file and return as list.""" + size = struct.unpack(' 0: + esize, etime, etype = struct.unpack(''}[fh.read(2)] + except IndexError: + raise ValueError("not a MicroManager TIFF file") + + results = {} + fh.seek(8) + (index_header, index_offset, display_header, display_offset, + comments_header, comments_offset, summary_header, summary_length + ) = struct.unpack(byteorder + "IIIIIIII", fh.read(32)) + + if summary_header != 2355492: + raise ValueError("invalid MicroManager summary_header") + results['summary'] = read_json(fh, byteorder, None, summary_length) + + if index_header != 54773648: + raise ValueError("invalid MicroManager index_header") + fh.seek(index_offset) + header, count = struct.unpack(byteorder + "II", fh.read(8)) + if header != 3453623: + raise ValueError("invalid MicroManager index_header") + data = struct.unpack(byteorder + "IIIII"*count, fh.read(20*count)) + results['index_map'] = { + 'channel': data[::5], 'slice': data[1::5], 'frame': data[2::5], + 'position': data[3::5], 'offset': data[4::5]} + + if display_header != 483765892: + raise ValueError("invalid MicroManager display_header") + fh.seek(display_offset) + header, count = struct.unpack(byteorder + "II", fh.read(8)) + if header != 347834724: + raise ValueError("invalid MicroManager display_header") + results['display_settings'] = read_json(fh, byteorder, None, count) + + if comments_header != 99384722: + raise ValueError("invalid MicroManager comments_header") + fh.seek(comments_offset) + header, count = struct.unpack(byteorder + "II", fh.read(8)) + if header != 84720485: + raise ValueError("invalid MicroManager comments_header") + results['comments'] = read_json(fh, byteorder, None, count) + + return results + + +def imagej_metadata(data, bytecounts, byteorder): + """Return dict from ImageJ metadata tag value.""" + _str = str if sys.version_info[0] < 3 else lambda x: str(x, 'cp1252') + + def read_string(data, byteorder): + return _str(stripnull(data[0 if byteorder == '<' else 1::2])) + + def read_double(data, byteorder): + return struct.unpack(byteorder+('d' * (len(data) // 8)), data) + + def read_bytes(data, byteorder): + #return struct.unpack('b' * len(data), data) + return numpy.fromstring(data, 'uint8') + + metadata_types = { # big endian + b'info': ('info', read_string), + b'labl': ('labels', read_string), + b'rang': ('ranges', read_double), + b'luts': ('luts', read_bytes), + b'roi ': ('roi', read_bytes), + b'over': ('overlays', read_bytes)} + metadata_types.update( # little endian + dict((k[::-1], v) for k, v in metadata_types.items())) + + if not bytecounts: + raise ValueError("no ImageJ metadata") + + if not data[:4] in (b'IJIJ', b'JIJI'): + raise ValueError("invalid ImageJ metadata") + + header_size = bytecounts[0] + if header_size < 12 or header_size > 804: + raise ValueError("invalid ImageJ metadata header size") + + ntypes = (header_size - 4) // 8 + header = struct.unpack(byteorder+'4sI'*ntypes, data[4:4+ntypes*8]) + pos = 4 + ntypes * 8 + counter = 0 + result = {} + for mtype, count in zip(header[::2], header[1::2]): + values = [] + name, func = metadata_types.get(mtype, (_str(mtype), read_bytes)) + for _ in range(count): + counter += 1 + pos1 = pos + bytecounts[counter] + values.append(func(data[pos:pos1], byteorder)) + pos = pos1 + result[name.strip()] = values[0] if count == 1 else values + return result + + +def imagej_description(description): + """Return dict from ImageJ image_description tag.""" + def _bool(val): + return {b'true': True, b'false': False}[val.lower()] + + _str = str if sys.version_info[0] < 3 else lambda x: str(x, 'cp1252') + result = {} + for line in description.splitlines(): + try: + key, val = line.split(b'=') + except Exception: + continue + key = key.strip() + val = val.strip() + for dtype in (int, float, _bool, _str): + try: + val = dtype(val) + break + except Exception: + pass + result[_str(key)] = val + return result + + +def _replace_by(module_function, package=None, warn=False): + """Try replace decorated function by module.function.""" + try: + from importlib import import_module + except ImportError: + warnings.warn('could not import module importlib') + return lambda func: func + + def decorate(func, module_function=module_function, warn=warn): + try: + module, function = module_function.split('.') + if not package: + module = import_module(module) + else: + module = import_module('.' + module, package=package) + func, oldfunc = getattr(module, function), func + globals()['__old_' + func.__name__] = oldfunc + except Exception: + if warn: + warnings.warn("failed to import %s" % module_function) + return func + + return decorate + + +def decodejpg(encoded, tables=b'', photometric=None, + ycbcr_subsampling=None, ycbcr_positioning=None): + """Decode JPEG encoded byte string (using _czifile extension module).""" + import _czifile + image = _czifile.decodejpg(encoded, tables) + if photometric == 'rgb' and ycbcr_subsampling and ycbcr_positioning: + # TODO: convert YCbCr to RGB + pass + return image.tostring() + + +@_replace_by('_tifffile.decodepackbits') +def decodepackbits(encoded): + """Decompress PackBits encoded byte string. + + PackBits is a simple byte-oriented run-length compression scheme. + + """ + func = ord if sys.version[0] == '2' else lambda x: x + result = [] + result_extend = result.extend + i = 0 + try: + while True: + n = func(encoded[i]) + 1 + i += 1 + if n < 129: + result_extend(encoded[i:i+n]) + i += n + elif n > 129: + result_extend(encoded[i:i+1] * (258-n)) + i += 1 + except IndexError: + pass + return b''.join(result) if sys.version[0] == '2' else bytes(result) + + +@_replace_by('_tifffile.decodelzw') +def decodelzw(encoded): + """Decompress LZW (Lempel-Ziv-Welch) encoded TIFF strip (byte string). + + The strip must begin with a CLEAR code and end with an EOI code. + + This is an implementation of the LZW decoding algorithm described in (1). + It is not compatible with old style LZW compressed files like quad-lzw.tif. + + """ + len_encoded = len(encoded) + bitcount_max = len_encoded * 8 + unpack = struct.unpack + + if sys.version[0] == '2': + newtable = [chr(i) for i in range(256)] + else: + newtable = [bytes([i]) for i in range(256)] + newtable.extend((0, 0)) + + def next_code(): + """Return integer of `bitw` bits at `bitcount` position in encoded.""" + start = bitcount // 8 + s = encoded[start:start+4] + try: + code = unpack('>I', s)[0] + except Exception: + code = unpack('>I', s + b'\x00'*(4-len(s)))[0] + code <<= bitcount % 8 + code &= mask + return code >> shr + + switchbitch = { # code: bit-width, shr-bits, bit-mask + 255: (9, 23, int(9*'1'+'0'*23, 2)), + 511: (10, 22, int(10*'1'+'0'*22, 2)), + 1023: (11, 21, int(11*'1'+'0'*21, 2)), + 2047: (12, 20, int(12*'1'+'0'*20, 2)), } + bitw, shr, mask = switchbitch[255] + bitcount = 0 + + if len_encoded < 4: + raise ValueError("strip must be at least 4 characters long") + + if next_code() != 256: + raise ValueError("strip must begin with CLEAR code") + + code = 0 + oldcode = 0 + result = [] + result_append = result.append + while True: + code = next_code() # ~5% faster when inlining this function + bitcount += bitw + if code == 257 or bitcount >= bitcount_max: # EOI + break + if code == 256: # CLEAR + table = newtable[:] + table_append = table.append + lentable = 258 + bitw, shr, mask = switchbitch[255] + code = next_code() + bitcount += bitw + if code == 257: # EOI + break + result_append(table[code]) + else: + if code < lentable: + decoded = table[code] + newcode = table[oldcode] + decoded[:1] + else: + newcode = table[oldcode] + newcode += newcode[:1] + decoded = newcode + result_append(decoded) + table_append(newcode) + lentable += 1 + oldcode = code + if lentable in switchbitch: + bitw, shr, mask = switchbitch[lentable] + + if code != 257: + warnings.warn("unexpected end of lzw stream (code %i)" % code) + + return b''.join(result) + + +@_replace_by('_tifffile.unpackints') +def unpackints(data, dtype, itemsize, runlen=0): + """Decompress byte string to array of integers of any bit size <= 32. + + Parameters + ---------- + data : byte str + Data to decompress. + dtype : numpy.dtype or str + A numpy boolean or integer type. + itemsize : int + Number of bits per integer. + runlen : int + Number of consecutive integers, after which to start at next byte. + + """ + if itemsize == 1: # bitarray + data = numpy.fromstring(data, '|B') + data = numpy.unpackbits(data) + if runlen % 8: + data = data.reshape(-1, runlen + (8 - runlen % 8)) + data = data[:, :runlen].reshape(-1) + return data.astype(dtype) + + dtype = numpy.dtype(dtype) + if itemsize in (8, 16, 32, 64): + return numpy.fromstring(data, dtype) + if itemsize < 1 or itemsize > 32: + raise ValueError("itemsize out of range: %i" % itemsize) + if dtype.kind not in "biu": + raise ValueError("invalid dtype") + + itembytes = next(i for i in (1, 2, 4, 8) if 8 * i >= itemsize) + if itembytes != dtype.itemsize: + raise ValueError("dtype.itemsize too small") + if runlen == 0: + runlen = len(data) // itembytes + skipbits = runlen*itemsize % 8 + if skipbits: + skipbits = 8 - skipbits + shrbits = itembytes*8 - itemsize + bitmask = int(itemsize*'1'+'0'*shrbits, 2) + dtypestr = '>' + dtype.char # dtype always big endian? + + unpack = struct.unpack + l = runlen * (len(data)*8 // (runlen*itemsize + skipbits)) + result = numpy.empty((l, ), dtype) + bitcount = 0 + for i in range(len(result)): + start = bitcount // 8 + s = data[start:start+itembytes] + try: + code = unpack(dtypestr, s)[0] + except Exception: + code = unpack(dtypestr, s + b'\x00'*(itembytes-len(s)))[0] + code <<= bitcount % 8 + code &= bitmask + result[i] = code >> shrbits + bitcount += itemsize + if (i+1) % runlen == 0: + bitcount += skipbits + return result + + +def unpackrgb(data, dtype='>> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff) + >>> print(unpackrgb(data, '>> print(unpackrgb(data, '>> print(unpackrgb(data, '= bits) + data = numpy.fromstring(data, dtype.byteorder+dt) + result = numpy.empty((data.size, len(bitspersample)), dtype.char) + for i, bps in enumerate(bitspersample): + t = data >> int(numpy.sum(bitspersample[i+1:])) + t &= int('0b'+'1'*bps, 2) + if rescale: + o = ((dtype.itemsize * 8) // bps + 1) * bps + if o > data.dtype.itemsize * 8: + t = t.astype('I') + t *= (2**o - 1) // (2**bps - 1) + t //= 2**(o - (dtype.itemsize * 8)) + result[:, i] = t + return result.reshape(-1) + + +def reorient(image, orientation): + """Return reoriented view of image array. + + Parameters + ---------- + image : numpy array + Non-squeezed output of asarray() functions. + Axes -3 and -2 must be image length and width respectively. + orientation : int or str + One of TIFF_ORIENTATIONS keys or values. + + """ + o = TIFF_ORIENTATIONS.get(orientation, orientation) + if o == 'top_left': + return image + elif o == 'top_right': + return image[..., ::-1, :] + elif o == 'bottom_left': + return image[..., ::-1, :, :] + elif o == 'bottom_right': + return image[..., ::-1, ::-1, :] + elif o == 'left_top': + return numpy.swapaxes(image, -3, -2) + elif o == 'right_top': + return numpy.swapaxes(image, -3, -2)[..., ::-1, :] + elif o == 'left_bottom': + return numpy.swapaxes(image, -3, -2)[..., ::-1, :, :] + elif o == 'right_bottom': + return numpy.swapaxes(image, -3, -2)[..., ::-1, ::-1, :] + + +def squeeze_axes(shape, axes, skip='XY'): + """Return shape and axes with single-dimensional entries removed. + + Remove unused dimensions unless their axes are listed in 'skip'. + + >>> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC') + ((5, 2, 1), 'TYX') + + """ + if len(shape) != len(axes): + raise ValueError("dimensions of axes and shape don't match") + shape, axes = zip(*(i for i in zip(shape, axes) + if i[0] > 1 or i[1] in skip)) + return shape, ''.join(axes) + + +def transpose_axes(data, axes, asaxes='CTZYX'): + """Return data with its axes permuted to match specified axes. + + A view is returned if possible. + + >>> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape + (5, 2, 1, 3, 4) + + """ + for ax in axes: + if ax not in asaxes: + raise ValueError("unknown axis %s" % ax) + # add missing axes to data + shape = data.shape + for ax in reversed(asaxes): + if ax not in axes: + axes = ax + axes + shape = (1,) + shape + data = data.reshape(shape) + # transpose axes + data = data.transpose([axes.index(ax) for ax in asaxes]) + return data + + +def stack_pages(pages, memmap=False, *args, **kwargs): + """Read data from sequence of TiffPage and stack them vertically. + + If memmap is True, return an array stored in a binary file on disk. + Additional parameters are passsed to the page asarray function. + + """ + if len(pages) == 0: + raise ValueError("no pages") + + if len(pages) == 1: + return pages[0].asarray(memmap=memmap, *args, **kwargs) + + result = pages[0].asarray(*args, **kwargs) + shape = (len(pages),) + result.shape + if memmap: + with tempfile.NamedTemporaryFile() as fh: + result = numpy.memmap(fh, dtype=result.dtype, shape=shape) + else: + result = numpy.empty(shape, dtype=result.dtype) + + for i, page in enumerate(pages): + result[i] = page.asarray(*args, **kwargs) + + return result + + +def stripnull(string): + """Return string truncated at first null character. + + Clean NULL terminated C strings. + + >>> stripnull(b'string\\x00') + b'string' + + """ + i = string.find(b'\x00') + return string if (i < 0) else string[:i] + + +def stripascii(string): + """Return string truncated at last byte that is 7bit ASCII. + + Clean NULL separated and terminated TIFF strings. + + >>> stripascii(b'string\\x00string\\n\\x01\\x00') + b'string\\x00string\\n' + >>> stripascii(b'\\x00') + b'' + + """ + # TODO: pythonize this + ord_ = ord if sys.version_info[0] < 3 else lambda x: x + i = len(string) + while i: + i -= 1 + if 8 < ord_(string[i]) < 127: + break + else: + i = -1 + return string[:i+1] + + +def format_size(size): + """Return file size as string from byte size.""" + for unit in ('B', 'KB', 'MB', 'GB', 'TB'): + if size < 2048: + return "%.f %s" % (size, unit) + size /= 1024.0 + + +def sequence(value): + """Return tuple containing value if value is not a sequence. + + >>> sequence(1) + (1,) + >>> sequence([1]) + [1] + + """ + try: + len(value) + return value + except TypeError: + return (value, ) + + +def product(iterable): + """Return product of sequence of numbers. + + Equivalent of functools.reduce(operator.mul, iterable, 1). + + >>> product([2**8, 2**30]) + 274877906944 + >>> product([]) + 1 + + """ + prod = 1 + for i in iterable: + prod *= i + return prod + + +def natural_sorted(iterable): + """Return human sorted list of strings. + + E.g. for sorting file names. + + >>> natural_sorted(['f1', 'f2', 'f10']) + ['f1', 'f2', 'f10'] + + """ + def sortkey(x): + return [(int(c) if c.isdigit() else c) for c in re.split(numbers, x)] + numbers = re.compile(r'(\d+)') + return sorted(iterable, key=sortkey) + + +def excel_datetime(timestamp, epoch=datetime.datetime.fromordinal(693594)): + """Return datetime object from timestamp in Excel serial format. + + Convert LSM time stamps. + + >>> excel_datetime(40237.029999999795) + datetime.datetime(2010, 2, 28, 0, 43, 11, 999982) + + """ + return epoch + datetime.timedelta(timestamp) + + +def julian_datetime(julianday, milisecond=0): + """Return datetime from days since 1/1/4713 BC and ms since midnight. + + Convert Julian dates according to MetaMorph. + + >>> julian_datetime(2451576, 54362783) + datetime.datetime(2000, 2, 2, 15, 6, 2, 783) + + """ + if julianday <= 1721423: + # no datetime before year 1 + return None + + a = julianday + 1 + if a > 2299160: + alpha = math.trunc((a - 1867216.25) / 36524.25) + a += 1 + alpha - alpha // 4 + b = a + (1524 if a > 1721423 else 1158) + c = math.trunc((b - 122.1) / 365.25) + d = math.trunc(365.25 * c) + e = math.trunc((b - d) / 30.6001) + + day = b - d - math.trunc(30.6001 * e) + month = e - (1 if e < 13.5 else 13) + year = c - (4716 if month > 2.5 else 4715) + + hour, milisecond = divmod(milisecond, 1000 * 60 * 60) + minute, milisecond = divmod(milisecond, 1000 * 60) + second, milisecond = divmod(milisecond, 1000) + + return datetime.datetime(year, month, day, + hour, minute, second, milisecond) + + +def test_tifffile(directory='testimages', verbose=True): + """Read all images in directory. + + Print error message on failure. + + >>> test_tifffile(verbose=False) + + """ + successful = 0 + failed = 0 + start = time.time() + for f in glob.glob(os.path.join(directory, '*.*')): + if verbose: + print("\n%s>\n" % f.lower(), end='') + t0 = time.time() + try: + tif = TiffFile(f, multifile=True) + except Exception as e: + if not verbose: + print(f, end=' ') + print("ERROR:", e) + failed += 1 + continue + try: + img = tif.asarray() + except ValueError: + try: + img = tif[0].asarray() + except Exception as e: + if not verbose: + print(f, end=' ') + print("ERROR:", e) + failed += 1 + continue + finally: + tif.close() + successful += 1 + if verbose: + print("%s, %s %s, %s, %.0f ms" % ( + str(tif), str(img.shape), img.dtype, tif[0].compression, + (time.time()-t0) * 1e3)) + if verbose: + print("\nSuccessfully read %i of %i files in %.3f s\n" % ( + successful, successful+failed, time.time()-start)) + + +class TIFF_SUBFILE_TYPES(object): + def __getitem__(self, key): + result = [] + if key & 1: + result.append('reduced_image') + if key & 2: + result.append('page') + if key & 4: + result.append('mask') + return tuple(result) + + +TIFF_PHOTOMETRICS = { + 0: 'miniswhite', + 1: 'minisblack', + 2: 'rgb', + 3: 'palette', + 4: 'mask', + 5: 'separated', # CMYK + 6: 'ycbcr', + 8: 'cielab', + 9: 'icclab', + 10: 'itulab', + 32803: 'cfa', # Color Filter Array + 32844: 'logl', + 32845: 'logluv', + 34892: 'linear_raw' +} + +TIFF_COMPESSIONS = { + 1: None, + 2: 'ccittrle', + 3: 'ccittfax3', + 4: 'ccittfax4', + 5: 'lzw', + 6: 'ojpeg', + 7: 'jpeg', + 8: 'adobe_deflate', + 9: 't85', + 10: 't43', + 32766: 'next', + 32771: 'ccittrlew', + 32773: 'packbits', + 32809: 'thunderscan', + 32895: 'it8ctpad', + 32896: 'it8lw', + 32897: 'it8mp', + 32898: 'it8bl', + 32908: 'pixarfilm', + 32909: 'pixarlog', + 32946: 'deflate', + 32947: 'dcs', + 34661: 'jbig', + 34676: 'sgilog', + 34677: 'sgilog24', + 34712: 'jp2000', + 34713: 'nef', +} + +TIFF_DECOMPESSORS = { + None: lambda x: x, + 'adobe_deflate': zlib.decompress, + 'deflate': zlib.decompress, + 'packbits': decodepackbits, + 'lzw': decodelzw, + # 'jpeg': decodejpg +} + +TIFF_DATA_TYPES = { + 1: '1B', # BYTE 8-bit unsigned integer. + 2: '1s', # ASCII 8-bit byte that contains a 7-bit ASCII code; + # the last byte must be NULL (binary zero). + 3: '1H', # SHORT 16-bit (2-byte) unsigned integer + 4: '1I', # LONG 32-bit (4-byte) unsigned integer. + 5: '2I', # RATIONAL Two LONGs: the first represents the numerator of + # a fraction; the second, the denominator. + 6: '1b', # SBYTE An 8-bit signed (twos-complement) integer. + 7: '1s', # UNDEFINED An 8-bit byte that may contain anything, + # depending on the definition of the field. + 8: '1h', # SSHORT A 16-bit (2-byte) signed (twos-complement) integer. + 9: '1i', # SLONG A 32-bit (4-byte) signed (twos-complement) integer. + 10: '2i', # SRATIONAL Two SLONGs: the first represents the numerator + # of a fraction, the second the denominator. + 11: '1f', # FLOAT Single precision (4-byte) IEEE format. + 12: '1d', # DOUBLE Double precision (8-byte) IEEE format. + 13: '1I', # IFD unsigned 4 byte IFD offset. + #14: '', # UNICODE + #15: '', # COMPLEX + 16: '1Q', # LONG8 unsigned 8 byte integer (BigTiff) + 17: '1q', # SLONG8 signed 8 byte integer (BigTiff) + 18: '1Q', # IFD8 unsigned 8 byte IFD offset (BigTiff) +} + +TIFF_SAMPLE_FORMATS = { + 1: 'uint', + 2: 'int', + 3: 'float', + #4: 'void', + #5: 'complex_int', + 6: 'complex', +} + +TIFF_SAMPLE_DTYPES = { + ('uint', 1): '?', # bitmap + ('uint', 2): 'B', + ('uint', 3): 'B', + ('uint', 4): 'B', + ('uint', 5): 'B', + ('uint', 6): 'B', + ('uint', 7): 'B', + ('uint', 8): 'B', + ('uint', 9): 'H', + ('uint', 10): 'H', + ('uint', 11): 'H', + ('uint', 12): 'H', + ('uint', 13): 'H', + ('uint', 14): 'H', + ('uint', 15): 'H', + ('uint', 16): 'H', + ('uint', 17): 'I', + ('uint', 18): 'I', + ('uint', 19): 'I', + ('uint', 20): 'I', + ('uint', 21): 'I', + ('uint', 22): 'I', + ('uint', 23): 'I', + ('uint', 24): 'I', + ('uint', 25): 'I', + ('uint', 26): 'I', + ('uint', 27): 'I', + ('uint', 28): 'I', + ('uint', 29): 'I', + ('uint', 30): 'I', + ('uint', 31): 'I', + ('uint', 32): 'I', + ('uint', 64): 'Q', + ('int', 8): 'b', + ('int', 16): 'h', + ('int', 32): 'i', + ('int', 64): 'q', + ('float', 16): 'e', + ('float', 32): 'f', + ('float', 64): 'd', + ('complex', 64): 'F', + ('complex', 128): 'D', + ('uint', (5, 6, 5)): 'B', +} + +TIFF_ORIENTATIONS = { + 1: 'top_left', + 2: 'top_right', + 3: 'bottom_right', + 4: 'bottom_left', + 5: 'left_top', + 6: 'right_top', + 7: 'right_bottom', + 8: 'left_bottom', +} + +# TODO: is there a standard for character axes labels? +AXES_LABELS = { + 'X': 'width', + 'Y': 'height', + 'Z': 'depth', + 'S': 'sample', # rgb(a) + 'I': 'series', # general sequence, plane, page, IFD + 'T': 'time', + 'C': 'channel', # color, emission wavelength + 'A': 'angle', + 'P': 'phase', # formerly F # P is Position in LSM! + 'R': 'tile', # region, point, mosaic + 'H': 'lifetime', # histogram + 'E': 'lambda', # excitation wavelength + 'L': 'exposure', # lux + 'V': 'event', + 'Q': 'other', + #'M': 'mosaic', # LSM 6 +} + +AXES_LABELS.update(dict((v, k) for k, v in AXES_LABELS.items())) + +# Map OME pixel types to numpy dtype +OME_PIXEL_TYPES = { + 'int8': 'i1', + 'int16': 'i2', + 'int32': 'i4', + 'uint8': 'u1', + 'uint16': 'u2', + 'uint32': 'u4', + 'float': 'f4', + # 'bit': 'bit', + 'double': 'f8', + 'complex': 'c8', + 'double-complex': 'c16', +} + +# NIH Image PicHeader v1.63 +NIH_IMAGE_HEADER = [ + ('fileid', 'a8'), + ('nlines', 'i2'), + ('pixelsperline', 'i2'), + ('version', 'i2'), + ('oldlutmode', 'i2'), + ('oldncolors', 'i2'), + ('colors', 'u1', (3, 32)), + ('oldcolorstart', 'i2'), + ('colorwidth', 'i2'), + ('extracolors', 'u2', (6, 3)), + ('nextracolors', 'i2'), + ('foregroundindex', 'i2'), + ('backgroundindex', 'i2'), + ('xscale', 'f8'), + ('_x0', 'i2'), + ('_x1', 'i2'), + ('units_t', 'i2'), # NIH_UNITS_TYPE + ('p1', [('x', 'i2'), ('y', 'i2')]), + ('p2', [('x', 'i2'), ('y', 'i2')]), + ('curvefit_t', 'i2'), # NIH_CURVEFIT_TYPE + ('ncoefficients', 'i2'), + ('coeff', 'f8', 6), + ('_um_len', 'u1'), + ('um', 'a15'), + ('_x2', 'u1'), + ('binarypic', 'b1'), + ('slicestart', 'i2'), + ('sliceend', 'i2'), + ('scalemagnification', 'f4'), + ('nslices', 'i2'), + ('slicespacing', 'f4'), + ('currentslice', 'i2'), + ('frameinterval', 'f4'), + ('pixelaspectratio', 'f4'), + ('colorstart', 'i2'), + ('colorend', 'i2'), + ('ncolors', 'i2'), + ('fill1', '3u2'), + ('fill2', '3u2'), + ('colortable_t', 'u1'), # NIH_COLORTABLE_TYPE + ('lutmode_t', 'u1'), # NIH_LUTMODE_TYPE + ('invertedtable', 'b1'), + ('zeroclip', 'b1'), + ('_xunit_len', 'u1'), + ('xunit', 'a11'), + ('stacktype_t', 'i2'), # NIH_STACKTYPE_TYPE +] + +NIH_COLORTABLE_TYPE = ( + 'CustomTable', 'AppleDefault', 'Pseudo20', 'Pseudo32', 'Rainbow', + 'Fire1', 'Fire2', 'Ice', 'Grays', 'Spectrum') + +NIH_LUTMODE_TYPE = ( + 'PseudoColor', 'OldAppleDefault', 'OldSpectrum', 'GrayScale', + 'ColorLut', 'CustomGrayscale') + +NIH_CURVEFIT_TYPE = ( + 'StraightLine', 'Poly2', 'Poly3', 'Poly4', 'Poly5', 'ExpoFit', + 'PowerFit', 'LogFit', 'RodbardFit', 'SpareFit1', 'Uncalibrated', + 'UncalibratedOD') + +NIH_UNITS_TYPE = ( + 'Nanometers', 'Micrometers', 'Millimeters', 'Centimeters', 'Meters', + 'Kilometers', 'Inches', 'Feet', 'Miles', 'Pixels', 'OtherUnits') + +NIH_STACKTYPE_TYPE = ( + 'VolumeStack', 'RGBStack', 'MovieStack', 'HSVStack') + +# Map Universal Imaging Corporation MetaMorph internal tag ids to name and type +UIC_TAGS = { + 0: ('auto_scale', int), + 1: ('min_scale', int), + 2: ('max_scale', int), + 3: ('spatial_calibration', int), + 4: ('x_calibration', Fraction), + 5: ('y_calibration', Fraction), + 6: ('calibration_units', str), + 7: ('name', str), + 8: ('thresh_state', int), + 9: ('thresh_state_red', int), + 10: ('tagid_10', None), # undefined + 11: ('thresh_state_green', int), + 12: ('thresh_state_blue', int), + 13: ('thresh_state_lo', int), + 14: ('thresh_state_hi', int), + 15: ('zoom', int), + 16: ('create_time', julian_datetime), + 17: ('last_saved_time', julian_datetime), + 18: ('current_buffer', int), + 19: ('gray_fit', None), + 20: ('gray_point_count', None), + 21: ('gray_x', Fraction), + 22: ('gray_y', Fraction), + 23: ('gray_min', Fraction), + 24: ('gray_max', Fraction), + 25: ('gray_unit_name', str), + 26: ('standard_lut', int), + 27: ('wavelength', int), + 28: ('stage_position', '(%i,2,2)u4'), # N xy positions as fractions + 29: ('camera_chip_offset', '(%i,2,2)u4'), # N xy offsets as fractions + 30: ('overlay_mask', None), + 31: ('overlay_compress', None), + 32: ('overlay', None), + 33: ('special_overlay_mask', None), + 34: ('special_overlay_compress', None), + 35: ('special_overlay', None), + 36: ('image_property', read_uic_image_property), + 37: ('stage_label', '%ip'), # N str + 38: ('autoscale_lo_info', Fraction), + 39: ('autoscale_hi_info', Fraction), + 40: ('absolute_z', '(%i,2)u4'), # N fractions + 41: ('absolute_z_valid', '(%i,)u4'), # N long + 42: ('gamma', int), + 43: ('gamma_red', int), + 44: ('gamma_green', int), + 45: ('gamma_blue', int), + 46: ('camera_bin', int), + 47: ('new_lut', int), + 48: ('image_property_ex', None), + 49: ('plane_property', int), + 50: ('user_lut_table', '(256,3)u1'), + 51: ('red_autoscale_info', int), + 52: ('red_autoscale_lo_info', Fraction), + 53: ('red_autoscale_hi_info', Fraction), + 54: ('red_minscale_info', int), + 55: ('red_maxscale_info', int), + 56: ('green_autoscale_info', int), + 57: ('green_autoscale_lo_info', Fraction), + 58: ('green_autoscale_hi_info', Fraction), + 59: ('green_minscale_info', int), + 60: ('green_maxscale_info', int), + 61: ('blue_autoscale_info', int), + 62: ('blue_autoscale_lo_info', Fraction), + 63: ('blue_autoscale_hi_info', Fraction), + 64: ('blue_min_scale_info', int), + 65: ('blue_max_scale_info', int), + #66: ('overlay_plane_color', read_uic_overlay_plane_color), +} + + +# Olympus FluoView +MM_DIMENSION = [ + ('name', 'a16'), + ('size', 'i4'), + ('origin', 'f8'), + ('resolution', 'f8'), + ('unit', 'a64'), +] + +MM_HEADER = [ + ('header_flag', 'i2'), + ('image_type', 'u1'), + ('image_name', 'a257'), + ('offset_data', 'u4'), + ('palette_size', 'i4'), + ('offset_palette0', 'u4'), + ('offset_palette1', 'u4'), + ('comment_size', 'i4'), + ('offset_comment', 'u4'), + ('dimensions', MM_DIMENSION, 10), + ('offset_position', 'u4'), + ('map_type', 'i2'), + ('map_min', 'f8'), + ('map_max', 'f8'), + ('min_value', 'f8'), + ('max_value', 'f8'), + ('offset_map', 'u4'), + ('gamma', 'f8'), + ('offset', 'f8'), + ('gray_channel', MM_DIMENSION), + ('offset_thumbnail', 'u4'), + ('voice_field', 'i4'), + ('offset_voice_field', 'u4'), +] + +# Carl Zeiss LSM +CZ_LSM_INFO = [ + ('magic_number', 'u4'), + ('structure_size', 'i4'), + ('dimension_x', 'i4'), + ('dimension_y', 'i4'), + ('dimension_z', 'i4'), + ('dimension_channels', 'i4'), + ('dimension_time', 'i4'), + ('data_type', 'i4'), # CZ_DATA_TYPES + ('thumbnail_x', 'i4'), + ('thumbnail_y', 'i4'), + ('voxel_size_x', 'f8'), + ('voxel_size_y', 'f8'), + ('voxel_size_z', 'f8'), + ('origin_x', 'f8'), + ('origin_y', 'f8'), + ('origin_z', 'f8'), + ('scan_type', 'u2'), + ('spectral_scan', 'u2'), + ('type_of_data', 'u4'), # CZ_TYPE_OF_DATA + ('offset_vector_overlay', 'u4'), + ('offset_input_lut', 'u4'), + ('offset_output_lut', 'u4'), + ('offset_channel_colors', 'u4'), + ('time_interval', 'f8'), + ('offset_channel_data_types', 'u4'), + ('offset_scan_info', 'u4'), # CZ_LSM_SCAN_INFO + ('offset_ks_data', 'u4'), + ('offset_time_stamps', 'u4'), + ('offset_event_list', 'u4'), + ('offset_roi', 'u4'), + ('offset_bleach_roi', 'u4'), + ('offset_next_recording', 'u4'), + # LSM 2.0 ends here + ('display_aspect_x', 'f8'), + ('display_aspect_y', 'f8'), + ('display_aspect_z', 'f8'), + ('display_aspect_time', 'f8'), + ('offset_mean_of_roi_overlay', 'u4'), + ('offset_topo_isoline_overlay', 'u4'), + ('offset_topo_profile_overlay', 'u4'), + ('offset_linescan_overlay', 'u4'), + ('offset_toolbar_flags', 'u4'), + ('offset_channel_wavelength', 'u4'), + ('offset_channel_factors', 'u4'), + ('objective_sphere_correction', 'f8'), + ('offset_unmix_parameters', 'u4'), + # LSM 3.2, 4.0 end here + ('offset_acquisition_parameters', 'u4'), + ('offset_characteristics', 'u4'), + ('offset_palette', 'u4'), + ('time_difference_x', 'f8'), + ('time_difference_y', 'f8'), + ('time_difference_z', 'f8'), + ('internal_use_1', 'u4'), + ('dimension_p', 'i4'), + ('dimension_m', 'i4'), + ('dimensions_reserved', '16i4'), + ('offset_tile_positions', 'u4'), + ('reserved_1', '9u4'), + ('offset_positions', 'u4'), + ('reserved_2', '21u4'), # must be 0 +] + +# Import functions for LSM_INFO sub-records +CZ_LSM_INFO_READERS = { + 'scan_info': read_cz_lsm_scan_info, + 'time_stamps': read_cz_lsm_time_stamps, + 'event_list': read_cz_lsm_event_list, + 'channel_colors': read_cz_lsm_floatpairs, + 'positions': read_cz_lsm_floatpairs, + 'tile_positions': read_cz_lsm_floatpairs, +} + +# Map cz_lsm_info.scan_type to dimension order +CZ_SCAN_TYPES = { + 0: 'XYZCT', # x-y-z scan + 1: 'XYZCT', # z scan (x-z plane) + 2: 'XYZCT', # line scan + 3: 'XYTCZ', # time series x-y + 4: 'XYZTC', # time series x-z + 5: 'XYTCZ', # time series 'Mean of ROIs' + 6: 'XYZTC', # time series x-y-z + 7: 'XYCTZ', # spline scan + 8: 'XYCZT', # spline scan x-z + 9: 'XYTCZ', # time series spline plane x-z + 10: 'XYZCT', # point mode +} + +# Map dimension codes to cz_lsm_info attribute +CZ_DIMENSIONS = { + 'X': 'dimension_x', + 'Y': 'dimension_y', + 'Z': 'dimension_z', + 'C': 'dimension_channels', + 'T': 'dimension_time', +} + +# Description of cz_lsm_info.data_type +CZ_DATA_TYPES = { + 0: 'varying data types', + 1: '8 bit unsigned integer', + 2: '12 bit unsigned integer', + 5: '32 bit float', +} + +# Description of cz_lsm_info.type_of_data +CZ_TYPE_OF_DATA = { + 0: 'Original scan data', + 1: 'Calculated data', + 2: '3D reconstruction', + 3: 'Topography height map', +} + +CZ_LSM_SCAN_INFO_ARRAYS = { + 0x20000000: "tracks", + 0x30000000: "lasers", + 0x60000000: "detection_channels", + 0x80000000: "illumination_channels", + 0xa0000000: "beam_splitters", + 0xc0000000: "data_channels", + 0x11000000: "timers", + 0x13000000: "markers", +} + +CZ_LSM_SCAN_INFO_STRUCTS = { + # 0x10000000: "recording", + 0x40000000: "track", + 0x50000000: "laser", + 0x70000000: "detection_channel", + 0x90000000: "illumination_channel", + 0xb0000000: "beam_splitter", + 0xd0000000: "data_channel", + 0x12000000: "timer", + 0x14000000: "marker", +} + +CZ_LSM_SCAN_INFO_ATTRIBUTES = { + # recording + 0x10000001: "name", + 0x10000002: "description", + 0x10000003: "notes", + 0x10000004: "objective", + 0x10000005: "processing_summary", + 0x10000006: "special_scan_mode", + 0x10000007: "scan_type", + 0x10000008: "scan_mode", + 0x10000009: "number_of_stacks", + 0x1000000a: "lines_per_plane", + 0x1000000b: "samples_per_line", + 0x1000000c: "planes_per_volume", + 0x1000000d: "images_width", + 0x1000000e: "images_height", + 0x1000000f: "images_number_planes", + 0x10000010: "images_number_stacks", + 0x10000011: "images_number_channels", + 0x10000012: "linscan_xy_size", + 0x10000013: "scan_direction", + 0x10000014: "time_series", + 0x10000015: "original_scan_data", + 0x10000016: "zoom_x", + 0x10000017: "zoom_y", + 0x10000018: "zoom_z", + 0x10000019: "sample_0x", + 0x1000001a: "sample_0y", + 0x1000001b: "sample_0z", + 0x1000001c: "sample_spacing", + 0x1000001d: "line_spacing", + 0x1000001e: "plane_spacing", + 0x1000001f: "plane_width", + 0x10000020: "plane_height", + 0x10000021: "volume_depth", + 0x10000023: "nutation", + 0x10000034: "rotation", + 0x10000035: "precession", + 0x10000036: "sample_0time", + 0x10000037: "start_scan_trigger_in", + 0x10000038: "start_scan_trigger_out", + 0x10000039: "start_scan_event", + 0x10000040: "start_scan_time", + 0x10000041: "stop_scan_trigger_in", + 0x10000042: "stop_scan_trigger_out", + 0x10000043: "stop_scan_event", + 0x10000044: "stop_scan_time", + 0x10000045: "use_rois", + 0x10000046: "use_reduced_memory_rois", + 0x10000047: "user", + 0x10000048: "use_bc_correction", + 0x10000049: "position_bc_correction1", + 0x10000050: "position_bc_correction2", + 0x10000051: "interpolation_y", + 0x10000052: "camera_binning", + 0x10000053: "camera_supersampling", + 0x10000054: "camera_frame_width", + 0x10000055: "camera_frame_height", + 0x10000056: "camera_offset_x", + 0x10000057: "camera_offset_y", + 0x10000059: "rt_binning", + 0x1000005a: "rt_frame_width", + 0x1000005b: "rt_frame_height", + 0x1000005c: "rt_region_width", + 0x1000005d: "rt_region_height", + 0x1000005e: "rt_offset_x", + 0x1000005f: "rt_offset_y", + 0x10000060: "rt_zoom", + 0x10000061: "rt_line_period", + 0x10000062: "prescan", + 0x10000063: "scan_direction_z", + # track + 0x40000001: "multiplex_type", # 0 after line; 1 after frame + 0x40000002: "multiplex_order", + 0x40000003: "sampling_mode", # 0 sample; 1 line average; 2 frame average + 0x40000004: "sampling_method", # 1 mean; 2 sum + 0x40000005: "sampling_number", + 0x40000006: "acquire", + 0x40000007: "sample_observation_time", + 0x4000000b: "time_between_stacks", + 0x4000000c: "name", + 0x4000000d: "collimator1_name", + 0x4000000e: "collimator1_position", + 0x4000000f: "collimator2_name", + 0x40000010: "collimator2_position", + 0x40000011: "is_bleach_track", + 0x40000012: "is_bleach_after_scan_number", + 0x40000013: "bleach_scan_number", + 0x40000014: "trigger_in", + 0x40000015: "trigger_out", + 0x40000016: "is_ratio_track", + 0x40000017: "bleach_count", + 0x40000018: "spi_center_wavelength", + 0x40000019: "pixel_time", + 0x40000021: "condensor_frontlens", + 0x40000023: "field_stop_value", + 0x40000024: "id_condensor_aperture", + 0x40000025: "condensor_aperture", + 0x40000026: "id_condensor_revolver", + 0x40000027: "condensor_filter", + 0x40000028: "id_transmission_filter1", + 0x40000029: "id_transmission1", + 0x40000030: "id_transmission_filter2", + 0x40000031: "id_transmission2", + 0x40000032: "repeat_bleach", + 0x40000033: "enable_spot_bleach_pos", + 0x40000034: "spot_bleach_posx", + 0x40000035: "spot_bleach_posy", + 0x40000036: "spot_bleach_posz", + 0x40000037: "id_tubelens", + 0x40000038: "id_tubelens_position", + 0x40000039: "transmitted_light", + 0x4000003a: "reflected_light", + 0x4000003b: "simultan_grab_and_bleach", + 0x4000003c: "bleach_pixel_time", + # laser + 0x50000001: "name", + 0x50000002: "acquire", + 0x50000003: "power", + # detection_channel + 0x70000001: "integration_mode", + 0x70000002: "special_mode", + 0x70000003: "detector_gain_first", + 0x70000004: "detector_gain_last", + 0x70000005: "amplifier_gain_first", + 0x70000006: "amplifier_gain_last", + 0x70000007: "amplifier_offs_first", + 0x70000008: "amplifier_offs_last", + 0x70000009: "pinhole_diameter", + 0x7000000a: "counting_trigger", + 0x7000000b: "acquire", + 0x7000000c: "point_detector_name", + 0x7000000d: "amplifier_name", + 0x7000000e: "pinhole_name", + 0x7000000f: "filter_set_name", + 0x70000010: "filter_name", + 0x70000013: "integrator_name", + 0x70000014: "channel_name", + 0x70000015: "detector_gain_bc1", + 0x70000016: "detector_gain_bc2", + 0x70000017: "amplifier_gain_bc1", + 0x70000018: "amplifier_gain_bc2", + 0x70000019: "amplifier_offset_bc1", + 0x70000020: "amplifier_offset_bc2", + 0x70000021: "spectral_scan_channels", + 0x70000022: "spi_wavelength_start", + 0x70000023: "spi_wavelength_stop", + 0x70000026: "dye_name", + 0x70000027: "dye_folder", + # illumination_channel + 0x90000001: "name", + 0x90000002: "power", + 0x90000003: "wavelength", + 0x90000004: "aquire", + 0x90000005: "detchannel_name", + 0x90000006: "power_bc1", + 0x90000007: "power_bc2", + # beam_splitter + 0xb0000001: "filter_set", + 0xb0000002: "filter", + 0xb0000003: "name", + # data_channel + 0xd0000001: "name", + 0xd0000003: "acquire", + 0xd0000004: "color", + 0xd0000005: "sample_type", + 0xd0000006: "bits_per_sample", + 0xd0000007: "ratio_type", + 0xd0000008: "ratio_track1", + 0xd0000009: "ratio_track2", + 0xd000000a: "ratio_channel1", + 0xd000000b: "ratio_channel2", + 0xd000000c: "ratio_const1", + 0xd000000d: "ratio_const2", + 0xd000000e: "ratio_const3", + 0xd000000f: "ratio_const4", + 0xd0000010: "ratio_const5", + 0xd0000011: "ratio_const6", + 0xd0000012: "ratio_first_images1", + 0xd0000013: "ratio_first_images2", + 0xd0000014: "dye_name", + 0xd0000015: "dye_folder", + 0xd0000016: "spectrum", + 0xd0000017: "acquire", + # timer + 0x12000001: "name", + 0x12000002: "description", + 0x12000003: "interval", + 0x12000004: "trigger_in", + 0x12000005: "trigger_out", + 0x12000006: "activation_time", + 0x12000007: "activation_number", + # marker + 0x14000001: "name", + 0x14000002: "description", + 0x14000003: "trigger_in", + 0x14000004: "trigger_out", +} + +# Map TIFF tag code to attribute name, default value, type, count, validator +TIFF_TAGS = { + 254: ('new_subfile_type', 0, 4, 1, TIFF_SUBFILE_TYPES()), + 255: ('subfile_type', None, 3, 1, + {0: 'undefined', 1: 'image', 2: 'reduced_image', 3: 'page'}), + 256: ('image_width', None, 4, 1, None), + 257: ('image_length', None, 4, 1, None), + 258: ('bits_per_sample', 1, 3, 1, None), + 259: ('compression', 1, 3, 1, TIFF_COMPESSIONS), + 262: ('photometric', None, 3, 1, TIFF_PHOTOMETRICS), + 266: ('fill_order', 1, 3, 1, {1: 'msb2lsb', 2: 'lsb2msb'}), + 269: ('document_name', None, 2, None, None), + 270: ('image_description', None, 2, None, None), + 271: ('make', None, 2, None, None), + 272: ('model', None, 2, None, None), + 273: ('strip_offsets', None, 4, None, None), + 274: ('orientation', 1, 3, 1, TIFF_ORIENTATIONS), + 277: ('samples_per_pixel', 1, 3, 1, None), + 278: ('rows_per_strip', 2**32-1, 4, 1, None), + 279: ('strip_byte_counts', None, 4, None, None), + 280: ('min_sample_value', None, 3, None, None), + 281: ('max_sample_value', None, 3, None, None), # 2**bits_per_sample + 282: ('x_resolution', None, 5, 1, None), + 283: ('y_resolution', None, 5, 1, None), + 284: ('planar_configuration', 1, 3, 1, {1: 'contig', 2: 'separate'}), + 285: ('page_name', None, 2, None, None), + 286: ('x_position', None, 5, 1, None), + 287: ('y_position', None, 5, 1, None), + 296: ('resolution_unit', 2, 4, 1, {1: 'none', 2: 'inch', 3: 'centimeter'}), + 297: ('page_number', None, 3, 2, None), + 305: ('software', None, 2, None, None), + 306: ('datetime', None, 2, None, None), + 315: ('artist', None, 2, None, None), + 316: ('host_computer', None, 2, None, None), + 317: ('predictor', 1, 3, 1, {1: None, 2: 'horizontal'}), + 318: ('white_point', None, 5, 2, None), + 319: ('primary_chromaticities', None, 5, 6, None), + 320: ('color_map', None, 3, None, None), + 322: ('tile_width', None, 4, 1, None), + 323: ('tile_length', None, 4, 1, None), + 324: ('tile_offsets', None, 4, None, None), + 325: ('tile_byte_counts', None, 4, None, None), + 338: ('extra_samples', None, 3, None, + {0: 'unspecified', 1: 'assocalpha', 2: 'unassalpha'}), + 339: ('sample_format', 1, 3, 1, TIFF_SAMPLE_FORMATS), + 340: ('smin_sample_value', None, None, None, None), + 341: ('smax_sample_value', None, None, None, None), + 347: ('jpeg_tables', None, 7, None, None), + 530: ('ycbcr_subsampling', 1, 3, 2, None), + 531: ('ycbcr_positioning', 1, 3, 1, None), + 32996: ('sgi_matteing', None, None, 1, None), # use extra_samples + 32996: ('sgi_datatype', None, None, 1, None), # use sample_format + 32997: ('image_depth', None, 4, 1, None), + 32998: ('tile_depth', None, 4, 1, None), + 33432: ('copyright', None, 1, None, None), + 33445: ('md_file_tag', None, 4, 1, None), + 33446: ('md_scale_pixel', None, 5, 1, None), + 33447: ('md_color_table', None, 3, None, None), + 33448: ('md_lab_name', None, 2, None, None), + 33449: ('md_sample_info', None, 2, None, None), + 33450: ('md_prep_date', None, 2, None, None), + 33451: ('md_prep_time', None, 2, None, None), + 33452: ('md_file_units', None, 2, None, None), + 33550: ('model_pixel_scale', None, 12, 3, None), + 33922: ('model_tie_point', None, 12, None, None), + 34665: ('exif_ifd', None, None, 1, None), + 34735: ('geo_key_directory', None, 3, None, None), + 34736: ('geo_double_params', None, 12, None, None), + 34737: ('geo_ascii_params', None, 2, None, None), + 34853: ('gps_ifd', None, None, 1, None), + 37510: ('user_comment', None, None, None, None), + 42112: ('gdal_metadata', None, 2, None, None), + 42113: ('gdal_nodata', None, 2, None, None), + 50289: ('mc_xy_position', None, 12, 2, None), + 50290: ('mc_z_position', None, 12, 1, None), + 50291: ('mc_xy_calibration', None, 12, 3, None), + 50292: ('mc_lens_lem_na_n', None, 12, 3, None), + 50293: ('mc_channel_name', None, 1, None, None), + 50294: ('mc_ex_wavelength', None, 12, 1, None), + 50295: ('mc_time_stamp', None, 12, 1, None), + 50838: ('imagej_byte_counts', None, None, None, None), + 65200: ('flex_xml', None, 2, None, None), + # code: (attribute name, default value, type, count, validator) +} + +# Map custom TIFF tag codes to attribute names and import functions +CUSTOM_TAGS = { + 700: ('xmp', read_bytes), + 34377: ('photoshop', read_numpy), + 33723: ('iptc', read_bytes), + 34675: ('icc_profile', read_bytes), + 33628: ('uic1tag', read_uic1tag), # Universal Imaging Corporation STK + 33629: ('uic2tag', read_uic2tag), + 33630: ('uic3tag', read_uic3tag), + 33631: ('uic4tag', read_uic4tag), + 34361: ('mm_header', read_mm_header), # Olympus FluoView + 34362: ('mm_stamp', read_mm_stamp), + 34386: ('mm_user_block', read_bytes), + 34412: ('cz_lsm_info', read_cz_lsm_info), # Carl Zeiss LSM + 43314: ('nih_image_header', read_nih_image_header), + # 40001: ('mc_ipwinscal', read_bytes), + 40100: ('mc_id_old', read_bytes), + 50288: ('mc_id', read_bytes), + 50296: ('mc_frame_properties', read_bytes), + 50839: ('imagej_metadata', read_bytes), + 51123: ('micromanager_metadata', read_json), +} + +# Max line length of printed output +PRINT_LINE_LEN = 79 + + +def imshow(data, title=None, vmin=0, vmax=None, cmap=None, + bitspersample=None, photometric='rgb', interpolation='nearest', + dpi=96, figure=None, subplot=111, maxdim=8192, **kwargs): + """Plot n-dimensional images using matplotlib.pyplot. + + Return figure, subplot and plot axis. + Requires pyplot already imported ``from matplotlib import pyplot``. + + Parameters + ---------- + bitspersample : int or None + Number of bits per channel in integer RGB images. + photometric : {'miniswhite', 'minisblack', 'rgb', or 'palette'} + The color space of the image data. + title : str + Window and subplot title. + figure : matplotlib.figure.Figure (optional). + Matplotlib to use for plotting. + subplot : int + A matplotlib.pyplot.subplot axis. + maxdim : int + maximum image size in any dimension. + kwargs : optional + Arguments for matplotlib.pyplot.imshow. + + """ + #if photometric not in ('miniswhite', 'minisblack', 'rgb', 'palette'): + # raise ValueError("Can't handle %s photometrics" % photometric) + # TODO: handle photometric == 'separated' (CMYK) + isrgb = photometric in ('rgb', 'palette') + data = numpy.atleast_2d(data.squeeze()) + data = data[(slice(0, maxdim), ) * len(data.shape)] + + dims = data.ndim + if dims < 2: + raise ValueError("not an image") + elif dims == 2: + dims = 0 + isrgb = False + else: + if isrgb and data.shape[-3] in (3, 4): + data = numpy.swapaxes(data, -3, -2) + data = numpy.swapaxes(data, -2, -1) + elif not isrgb and (data.shape[-1] < data.shape[-2] // 16 and + data.shape[-1] < data.shape[-3] // 16 and + data.shape[-1] < 5): + data = numpy.swapaxes(data, -3, -1) + data = numpy.swapaxes(data, -2, -1) + isrgb = isrgb and data.shape[-1] in (3, 4) + dims -= 3 if isrgb else 2 + + if photometric == 'palette' and isrgb: + datamax = data.max() + if datamax > 255: + data >>= 8 # possible precision loss + data = data.astype('B') + elif data.dtype.kind in 'ui': + if not (isrgb and data.dtype.itemsize <= 1) or bitspersample is None: + try: + bitspersample = int(math.ceil(math.log(data.max(), 2))) + except Exception: + bitspersample = data.dtype.itemsize * 8 + elif not isinstance(bitspersample, int): + # bitspersample can be tuple, e.g. (5, 6, 5) + bitspersample = data.dtype.itemsize * 8 + datamax = 2**bitspersample + if isrgb: + if bitspersample < 8: + data <<= 8 - bitspersample + elif bitspersample > 8: + data >>= bitspersample - 8 # precision loss + data = data.astype('B') + elif data.dtype.kind == 'f': + datamax = data.max() + if isrgb and datamax > 1.0: + if data.dtype.char == 'd': + data = data.astype('f') + data /= datamax + elif data.dtype.kind == 'b': + datamax = 1 + elif data.dtype.kind == 'c': + raise NotImplementedError("complex type") # TODO: handle complex types + + if not isrgb: + if vmax is None: + vmax = datamax + if vmin is None: + if data.dtype.kind == 'i': + dtmin = numpy.iinfo(data.dtype).min + vmin = numpy.min(data) + if vmin == dtmin: + vmin = numpy.min(data > dtmin) + if data.dtype.kind == 'f': + dtmin = numpy.finfo(data.dtype).min + vmin = numpy.min(data) + if vmin == dtmin: + vmin = numpy.min(data > dtmin) + else: + vmin = 0 + + pyplot = sys.modules['matplotlib.pyplot'] + + if figure is None: + pyplot.rc('font', family='sans-serif', weight='normal', size=8) + figure = pyplot.figure(dpi=dpi, figsize=(10.3, 6.3), frameon=True, + facecolor='1.0', edgecolor='w') + try: + figure.canvas.manager.window.title(title) + except Exception: + pass + pyplot.subplots_adjust(bottom=0.03*(dims+2), top=0.9, + left=0.1, right=0.95, hspace=0.05, wspace=0.0) + subplot = pyplot.subplot(subplot) + + if title: + try: + title = unicode(title, 'Windows-1252') + except TypeError: + pass + pyplot.title(title, size=11) + + if cmap is None: + if data.dtype.kind in 'ubf' or vmin == 0: + cmap = 'cubehelix' + else: + cmap = 'coolwarm' + if photometric == 'miniswhite': + cmap += '_r' + + image = pyplot.imshow(data[(0, ) * dims].squeeze(), vmin=vmin, vmax=vmax, + cmap=cmap, interpolation=interpolation, **kwargs) + + if not isrgb: + pyplot.colorbar() # panchor=(0.55, 0.5), fraction=0.05 + + def format_coord(x, y): + # callback function to format coordinate display in toolbar + x = int(x + 0.5) + y = int(y + 0.5) + try: + if dims: + return "%s @ %s [%4i, %4i]" % (cur_ax_dat[1][y, x], + current, x, y) + else: + return "%s @ [%4i, %4i]" % (data[y, x], x, y) + except IndexError: + return "" + + pyplot.gca().format_coord = format_coord + + if dims: + current = list((0, ) * dims) + cur_ax_dat = [0, data[tuple(current)].squeeze()] + sliders = [pyplot.Slider( + pyplot.axes([0.125, 0.03*(axis+1), 0.725, 0.025]), + 'Dimension %i' % axis, 0, data.shape[axis]-1, 0, facecolor='0.5', + valfmt='%%.0f [%i]' % data.shape[axis]) for axis in range(dims)] + for slider in sliders: + slider.drawon = False + + def set_image(current, sliders=sliders, data=data): + # change image and redraw canvas + cur_ax_dat[1] = data[tuple(current)].squeeze() + image.set_data(cur_ax_dat[1]) + for ctrl, index in zip(sliders, current): + ctrl.eventson = False + ctrl.set_val(index) + ctrl.eventson = True + figure.canvas.draw() + + def on_changed(index, axis, data=data, current=current): + # callback function for slider change event + index = int(round(index)) + cur_ax_dat[0] = axis + if index == current[axis]: + return + if index >= data.shape[axis]: + index = 0 + elif index < 0: + index = data.shape[axis] - 1 + current[axis] = index + set_image(current) + + def on_keypressed(event, data=data, current=current): + # callback function for key press event + key = event.key + axis = cur_ax_dat[0] + if str(key) in '0123456789': + on_changed(key, axis) + elif key == 'right': + on_changed(current[axis] + 1, axis) + elif key == 'left': + on_changed(current[axis] - 1, axis) + elif key == 'up': + cur_ax_dat[0] = 0 if axis == len(data.shape)-1 else axis + 1 + elif key == 'down': + cur_ax_dat[0] = len(data.shape)-1 if axis == 0 else axis - 1 + elif key == 'end': + on_changed(data.shape[axis] - 1, axis) + elif key == 'home': + on_changed(0, axis) + + figure.canvas.mpl_connect('key_press_event', on_keypressed) + for axis, ctrl in enumerate(sliders): + ctrl.on_changed(lambda k, a=axis: on_changed(k, a)) + + return figure, subplot, image + + +def _app_show(): + """Block the GUI. For use as skimage plugin.""" + pyplot = sys.modules['matplotlib.pyplot'] + pyplot.show() + + +def main(argv=None): + """Command line usage main function.""" + if float(sys.version[0:3]) < 2.6: + print("This script requires Python version 2.6 or better.") + print("This is Python version %s" % sys.version) + return 0 + if argv is None: + argv = sys.argv + + import optparse + + parser = optparse.OptionParser( + usage="usage: %prog [options] path", + description="Display image data in TIFF files.", + version="%%prog %s" % __version__) + opt = parser.add_option + opt('-p', '--page', dest='page', type='int', default=-1, + help="display single page") + opt('-s', '--series', dest='series', type='int', default=-1, + help="display series of pages of same shape") + opt('--nomultifile', dest='nomultifile', action='store_true', + default=False, help="don't read OME series from multiple files") + opt('--noplot', dest='noplot', action='store_true', default=False, + help="don't display images") + opt('--interpol', dest='interpol', metavar='INTERPOL', default='bilinear', + help="image interpolation method") + opt('--dpi', dest='dpi', type='int', default=96, + help="set plot resolution") + opt('--debug', dest='debug', action='store_true', default=False, + help="raise exception on failures") + opt('--test', dest='test', action='store_true', default=False, + help="try read all images in path") + opt('--doctest', dest='doctest', action='store_true', default=False, + help="runs the docstring examples") + opt('-v', '--verbose', dest='verbose', action='store_true', default=True) + opt('-q', '--quiet', dest='verbose', action='store_false') + + settings, path = parser.parse_args() + path = ' '.join(path) + + if settings.doctest: + import doctest + doctest.testmod() + return 0 + if not path: + parser.error("No file specified") + if settings.test: + test_tifffile(path, settings.verbose) + return 0 + + if any(i in path for i in '?*'): + path = glob.glob(path) + if not path: + print('no files match the pattern') + return 0 + # TODO: handle image sequences + #if len(path) == 1: + path = path[0] + + print("Reading file structure...", end=' ') + start = time.time() + try: + tif = TiffFile(path, multifile=not settings.nomultifile) + except Exception as e: + if settings.debug: + raise + else: + print("\n", e) + sys.exit(0) + print("%.3f ms" % ((time.time()-start) * 1e3)) + + if tif.is_ome: + settings.norgb = True + + images = [(None, tif[0 if settings.page < 0 else settings.page])] + if not settings.noplot: + print("Reading image data... ", end=' ') + + def notnone(x): + return next(i for i in x if i is not None) + start = time.time() + try: + if settings.page >= 0: + images = [(tif.asarray(key=settings.page), + tif[settings.page])] + elif settings.series >= 0: + images = [(tif.asarray(series=settings.series), + notnone(tif.series[settings.series].pages))] + else: + images = [] + for i, s in enumerate(tif.series): + try: + images.append( + (tif.asarray(series=i), notnone(s.pages))) + except ValueError as e: + images.append((None, notnone(s.pages))) + if settings.debug: + raise + else: + print("\n* series %i failed: %s... " % (i, e), + end='') + print("%.3f ms" % ((time.time()-start) * 1e3)) + except Exception as e: + if settings.debug: + raise + else: + print(e) + + tif.close() + + print("\nTIFF file:", tif) + print() + for i, s in enumerate(tif.series): + print ("Series %i" % i) + print(s) + print() + for i, page in images: + print(page) + print(page.tags) + if page.is_palette: + print("\nColor Map:", page.color_map.shape, page.color_map.dtype) + for attr in ('cz_lsm_info', 'cz_lsm_scan_info', 'uic_tags', + 'mm_header', 'imagej_tags', 'micromanager_metadata', + 'nih_image_header'): + if hasattr(page, attr): + print("", attr.upper(), Record(getattr(page, attr)), sep="\n") + print() + if page.is_micromanager: + print('MICROMANAGER_FILE_METADATA') + print(Record(tif.micromanager_metadata)) + + if images and not settings.noplot: + try: + import matplotlib + matplotlib.use('TkAgg') + from matplotlib import pyplot + except ImportError as e: + warnings.warn("failed to import matplotlib.\n%s" % e) + else: + for img, page in images: + if img is None: + continue + vmin, vmax = None, None + if 'gdal_nodata' in page.tags: + try: + vmin = numpy.min(img[img > float(page.gdal_nodata)]) + except ValueError: + pass + if page.is_stk: + try: + vmin = page.uic_tags['min_scale'] + vmax = page.uic_tags['max_scale'] + except KeyError: + pass + else: + if vmax <= vmin: + vmin, vmax = None, None + title = "%s\n %s" % (str(tif), str(page)) + imshow(img, title=title, vmin=vmin, vmax=vmax, + bitspersample=page.bits_per_sample, + photometric=page.photometric, + interpolation=settings.interpol, + dpi=settings.dpi) + pyplot.show() + + +TIFFfile = TiffFile # backwards compatibility + +if sys.version_info[0] > 2: + basestring = str, bytes + unicode = str + +if __name__ == "__main__": + sys.exit(main()) + diff --git a/skimage/io/tests/test_io.py b/skimage/io/tests/test_io.py index 14879e64..81e7830f 100644 --- a/skimage/io/tests/test_io.py +++ b/skimage/io/tests/test_io.py @@ -29,17 +29,5 @@ def test_imread_url(): assert image.shape == (512, 512) -@raises(RuntimeError) -def test_imread_no_plugin(): - # tweak data path so that file URI works on both unix and windows. - image_path = os.path.join(data_dir, 'lena.png') - plugins = plugin_store['imread'] - plugin_store['imread'] = [] - try: - io.imread(image_path) - finally: - plugin_store['imread'] = plugins - - if __name__ == "__main__": run_module_suite() diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index 20ec5b8e..09b56c07 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -8,19 +8,13 @@ from tempfile import NamedTemporaryFile from skimage import data_dir from skimage.io import (imread, imsave, use_plugin, reset_plugins, Image as ioImage) -from skimage.io.test.utils import ubyte_check, full_range_check +from skimage.io.tests.utils import ubyte_check, full_range_check from six import BytesIO - -try: - from PIL import Image - from skimage.io._plugins.pil_plugin import pil_to_ndarray, ndarray_to_pil, _palette_is_grayscale - use_plugin('pil') -except ImportError: - PIL_available = False -else: - PIL_available = True +from PIL import Image +from skimage.io._plugins.pil_plugin import pil_to_ndarray, ndarray_to_pil, _palette_is_grayscale +use_plugin('pil') np.random.seed(0) @@ -40,8 +34,6 @@ def setup_module(self): pass - -@skipif(not PIL_available) def test_imread_flatten(): # a color image is flattened img = imread(os.path.join(data_dir, 'color.png'), flatten=True) @@ -52,7 +44,6 @@ def test_imread_flatten(): assert np.sctype2char(img.dtype) in np.typecodes['AllInteger'] -@skipif(not PIL_available) def test_imread_palette(): img = imread(os.path.join(data_dir, 'palette_gray.png')) assert img.ndim == 2 @@ -60,7 +51,6 @@ def test_imread_palette(): assert img.ndim == 3 -@skipif(not PIL_available) def test_palette_is_gray(): gray = Image.open(os.path.join(data_dir, 'palette_gray.png')) assert _palette_is_grayscale(gray) @@ -68,7 +58,6 @@ def test_palette_is_gray(): assert not _palette_is_grayscale(color) -@skipif(not PIL_available) def test_bilevel(): expected = np.zeros((10, 10)) expected[::2] = 255 @@ -77,7 +66,6 @@ def test_bilevel(): assert_array_equal(img, expected) -@skipif(not PIL_available) def test_imread_uint16(): expected = np.load(os.path.join(data_dir, 'chessboard_GRAY_U8.npy')) img = imread(os.path.join(data_dir, 'chessboard_GRAY_U16.tif')) @@ -85,7 +73,6 @@ def test_imread_uint16(): assert_array_almost_equal(img, expected) -@skipif(not PIL_available) def test_repr_png(): img_path = os.path.join(data_dir, 'camera.png') original_img = ioImage(imread(img_path)) @@ -98,14 +85,12 @@ def test_repr_png(): assert np.all(original_img == round_trip) -@skipif(not PIL_available) + def test_imread_truncated_jpg(): assert_raises((IOError, ValueError), imread, os.path.join(data_dir, 'truncated.jpg')) -# Big endian images not correctly loaded for PIL < 1.1.7 -# Renable test when PIL 1.1.7 is more common. -@skipif(True) + def test_imread_uint16_big_endian(): expected = np.load(os.path.join(data_dir, 'chessboard_GRAY_U8.npy')) img = imread(os.path.join(data_dir, 'chessboard_GRAY_U16B.tif')) @@ -141,11 +126,9 @@ class TestSave: x = (x * 255).astype(dtype) yield self.verify_roundtrip, dtype, x, roundtrip_function(x) - @skipif(not PIL_available) def test_imsave_roundtrip_file(self): self.verify_imsave_roundtrip(self.roundtrip_file) - @skipif(not PIL_available) def test_imsave_roundtrip_pil_image(self): self.verify_imsave_roundtrip(self.roundtrip_pil_image) @@ -175,17 +158,35 @@ def test_imexport_imimport(): assert out.shape == shape -@skip(not PIL_available) -def test_all_color(self): +@skipif(not PIL_available) +def test_all_color(): ubyte_check('pil') ubyte_check('pil', 'bmp') -@skip(not PIL_available) -def test_all_mono(self): +@skipif(not PIL_available) +def test_all_mono(): full_range_check('pil') full_range_check('pil', 'tiff') +class TestSaveTIF: + def roundtrip(self, dtype, x): + f = NamedTemporaryFile(suffix='.tif') + fname = f.name + f.close() + sio.imsave(fname, x) + y = sio.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.float32, np.float64): + x = np.ones(shape, dtype=dtype) * np.random.rand(*shape) + + if not np.issubdtype(dtype, float): + x = (x * 255).astype(dtype) + yield self.roundtrip, dtype, x + if __name__ == "__main__": run_module_suite() diff --git a/skimage/io/tests/utils.py b/skimage/io/tests/utils.py index 1c4482f3..faba8754 100644 --- a/skimage/io/tests/utils.py +++ b/skimage/io/tests/utils.py @@ -29,7 +29,7 @@ def ubyte_check(plugin, fmt='png'): img2 = img > 128 r2 = roundtrip(img2, plugin, fmt) - testing.assert_allclose(img2, img_as_bool(r2)) + testing.assert_allclose(img2.astype(np.uint8), r2) img3 = img_as_float(img) r3 = roundtrip(img3, plugin, fmt) From d9e169c518eea52f904a33c15ccc1d31c479a0e8 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 6 Oct 2014 21:18:12 -0500 Subject: [PATCH 0505/1122] Fix PIL tests, move helpers to _shared, fix widgets test --- skimage/_shared/testing.py | 82 ++++++++++++++++++++++++++++ skimage/io/tests/__init__.py | 0 skimage/io/tests/test_pil.py | 12 ++-- skimage/io/tests/utils.py | 82 ---------------------------- skimage/viewer/tests/test_widgets.py | 10 ++-- 5 files changed, 91 insertions(+), 95 deletions(-) delete mode 100644 skimage/io/tests/__init__.py delete mode 100644 skimage/io/tests/utils.py diff --git a/skimage/_shared/testing.py b/skimage/_shared/testing.py index 9b42c582..be58540a 100644 --- a/skimage/_shared/testing.py +++ b/skimage/_shared/testing.py @@ -2,6 +2,12 @@ import re +from tempfile import NamedTemporaryFile + +from skimage import ( + data, io, img_as_uint, img_as_bool, img_as_float, img_as_int, img_as_ubyte) +from numpy import testing +import numpy as np SKIP_RE = re.compile("(\s*>>>.*?)(\s*)#\s*skip\s+if\s+(.*)$") @@ -75,3 +81,79 @@ def doctest_skip_parser(func): new_lines.append(code) func.__doc__ = "\n".join(new_lines) return func + + +def roundtrip(img, plugin, suffix): + """Save and read an image using a specified plugin""" + if not '.' in suffix: + suffix = '.' + suffix + temp_file = NamedTemporaryFile(suffix=suffix, delete=False) + temp_file.close() + fname = temp_file.name + io.imsave(fname, img, plugin=plugin) + return io.imread(fname, plugin=plugin) + + +def ubyte_check(plugin, fmt='png'): + """Check roundtrip behavior for images that can only be saved as uint8 + + All major input types should be handled as ubytes and read + back correctly. + """ + img = img_as_ubyte(data.chelsea()) + r1 = roundtrip(img, plugin, fmt) + testing.assert_allclose(img, r1) + + img2 = img > 128 + r2 = roundtrip(img2, plugin, fmt) + testing.assert_allclose(img2.astype(np.uint8), r2) + + img3 = img_as_float(img) + r3 = roundtrip(img3, plugin, fmt) + testing.assert_allclose(r3, img) + + img4 = img_as_int(img) + r4 = roundtrip(img4, plugin, fmt) + testing.assert_allclose(r4, img) + + img5 = img_as_uint(img) + r5 = roundtrip(img5, plugin, fmt) + testing.assert_allclose(r5, img) + + +def full_range_check(plugin, fmt='png'): + """Check the roundtrip behavior for images that support most types. + + All major input types should be handled, except bool is treated + as ubyte and float can treated as uint16 or float. + """ + + img = img_as_ubyte(data.moon()) + r1 = roundtrip(img, plugin, fmt) + testing.assert_allclose(img, r1) + + img2 = img > 128 + r2 = roundtrip(img2, plugin, fmt) + testing.assert_allclose(img2.astype(np.uint8), r2) + + img3 = img_as_float(img) + r3 = roundtrip(img3, plugin, fmt) + if r3.dtype.kind == 'f': + testing.assert_allclose(img3, r3) + else: + testing.assert_allclose(r3, img_as_uint(img)) + + img4 = img_as_int(img) + r4 = roundtrip(img4, plugin, fmt) + testing.assert_allclose(r4, img4) + + img5 = img_as_uint(img) + r5 = roundtrip(img5, plugin, fmt) + testing.assert_allclose(r5, img5) + + +if __name__ == '__main__': + ubyte_check('pil') + full_range_check('pil') + ubyte_check('pil', 'bmp') + full_range_check('pil', 'tiff') diff --git a/skimage/io/tests/__init__.py b/skimage/io/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index 09b56c07..7b5cdd49 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -8,7 +8,7 @@ from tempfile import NamedTemporaryFile from skimage import data_dir from skimage.io import (imread, imsave, use_plugin, reset_plugins, Image as ioImage) -from skimage.io.tests.utils import ubyte_check, full_range_check +from skimage._shared.testing import ubyte_check, full_range_check from six import BytesIO @@ -94,7 +94,7 @@ def test_imread_truncated_jpg(): def test_imread_uint16_big_endian(): expected = np.load(os.path.join(data_dir, 'chessboard_GRAY_U8.npy')) img = imread(os.path.join(data_dir, 'chessboard_GRAY_U16B.tif')) - assert img.dtype == np.dtype('>u2') + assert img.dtype == np.uint16 assert_array_almost_equal(img, expected) @@ -133,7 +133,6 @@ class TestSave: self.verify_imsave_roundtrip(self.roundtrip_pil_image) -@skipif(not PIL_available) def test_imsave_filelike(): shape = (2, 2) image = np.zeros(shape) @@ -149,7 +148,6 @@ def test_imsave_filelike(): assert_allclose(out, image) -@skipif(not PIL_available) def test_imexport_imimport(): shape = (2, 2) image = np.zeros(shape) @@ -158,13 +156,11 @@ def test_imexport_imimport(): assert out.shape == shape -@skipif(not PIL_available) def test_all_color(): ubyte_check('pil') ubyte_check('pil', 'bmp') -@skipif(not PIL_available) def test_all_mono(): full_range_check('pil') full_range_check('pil', 'tiff') @@ -175,8 +171,8 @@ class TestSaveTIF: f = NamedTemporaryFile(suffix='.tif') fname = f.name f.close() - sio.imsave(fname, x) - y = sio.imread(fname) + imsave(fname, x) + y = imread(fname) assert_array_equal(x, y) def test_imsave_roundtrip(self): diff --git a/skimage/io/tests/utils.py b/skimage/io/tests/utils.py deleted file mode 100644 index faba8754..00000000 --- a/skimage/io/tests/utils.py +++ /dev/null @@ -1,82 +0,0 @@ -from tempfile import NamedTemporaryFile - -from skimage import ( - data, io, img_as_uint, img_as_bool, img_as_float, img_as_int, img_as_ubyte) -from numpy import testing -import numpy as np - - -def roundtrip(img, plugin, suffix): - """Save and read an image using a specified plugin""" - if not '.' in suffix: - suffix = '.' + suffix - temp_file = NamedTemporaryFile(suffix=suffix, delete=False) - temp_file.close() - fname = temp_file.name - io.imsave(fname, img, plugin=plugin) - return io.imread(fname, plugin=plugin) - - -def ubyte_check(plugin, fmt='png'): - """Check roundtrip behavior for images that can only be saved as uint8 - - All major input types should be handled as ubytes and read - back correctly. - """ - img = img_as_ubyte(data.chelsea()) - r1 = roundtrip(img, plugin, fmt) - testing.assert_allclose(img, r1) - - img2 = img > 128 - r2 = roundtrip(img2, plugin, fmt) - testing.assert_allclose(img2.astype(np.uint8), r2) - - img3 = img_as_float(img) - r3 = roundtrip(img3, plugin, fmt) - testing.assert_allclose(r3, img) - - img4 = img_as_int(img) - r4 = roundtrip(img4, plugin, fmt) - testing.assert_allclose(r4, img) - - img5 = img_as_uint(img) - r5 = roundtrip(img5, plugin, fmt) - testing.assert_allclose(r5, img) - - -def full_range_check(plugin, fmt='png'): - """Check the roundtrip behavior for images that support most types. - - All major input types should be handled, except bool is treated - as ubyte and float can treated as uint16 or float. - """ - - img = img_as_ubyte(data.moon()) - r1 = roundtrip(img, plugin, fmt) - testing.assert_allclose(img, r1) - - img2 = img > 128 - r2 = roundtrip(img2, plugin, fmt) - testing.assert_allclose(img2.astype(np.uint8), r2) - - img3 = img_as_float(img) - r3 = roundtrip(img3, plugin, fmt) - if r3.dtype.kind == 'f': - testing.assert_allclose(img3, r3) - else: - testing.assert_allclose(r3, img_as_uint(img)) - - img4 = img_as_int(img) - r4 = roundtrip(img4, plugin, fmt) - testing.assert_allclose(r4, img4) - - img5 = img_as_uint(img) - r5 = roundtrip(img5, plugin, fmt) - testing.assert_allclose(r5, img5) - - -if __name__ == '__main__': - ubyte_check('pil') - full_range_check('pil') - ubyte_check('pil', 'bmp') - full_range_check('pil', 'tiff') diff --git a/skimage/viewer/tests/test_widgets.py b/skimage/viewer/tests/test_widgets.py index a9f84fbd..ba76b7c5 100644 --- a/skimage/viewer/tests/test_widgets.py +++ b/skimage/viewer/tests/test_widgets.py @@ -1,6 +1,6 @@ import os -from skimage import data, img_as_float, io +from skimage import data, img_as_float, io, img_as_uint from skimage.viewer import ImageViewer, viewer_available from skimage.viewer.widgets import ( Slider, OKCancelButtons, SaveButtons, ComboBox, CheckBox, Text) @@ -27,10 +27,10 @@ def test_check_box(): cb.val = False assert_equal(cb.val, False) cb.val = 1 - assert_equal(cb.val, True) + assert_equal(cb.val, True) cb.val = 0 assert_equal(cb.val, False) - + @skipif(not viewer_available) def test_combo_box(): @@ -101,8 +101,8 @@ def test_save_buttons(): sv.save_to_stack() sv.save_to_file(filename) - img = img_as_float(data.imread(filename)) - assert_almost_equal(img, viewer.image) + img = data.imread(filename) + assert_almost_equal(img, img_as_uint(viewer.image)) img = io.pop() assert_almost_equal(img, viewer.image) From f938d294f570825089bc3d232c8a140e01e24f6b Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 7 Oct 2014 04:55:20 -0500 Subject: [PATCH 0506/1122] Fix deprecation warning in draw.py from numpy 1.9 From 1a1a7cac53c1a1884dd235d3c3a7875ebfc0adf5 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 7 Oct 2014 05:28:01 -0500 Subject: [PATCH 0507/1122] Ignore tifffile.py in coveragerc --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 5a372d61..992eafdc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,6 +6,7 @@ source = skimage include = */skimage/* omit = */setup.py + */skimage/io/_plugins/tifffile.py [report] exclude_lines = From fa5a575743d7e271a4986f3673898d677dccdd15 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 7 Oct 2014 06:59:29 -0500 Subject: [PATCH 0508/1122] Reinstate tifffile_plugin for backwards compatibility --- skimage/io/_plugins/tifffile_plugin.ini | 3 +++ skimage/io/_plugins/tifffile_plugin.py | 6 ++++++ 2 files changed, 9 insertions(+) create mode 100644 skimage/io/_plugins/tifffile_plugin.ini create mode 100644 skimage/io/_plugins/tifffile_plugin.py diff --git a/skimage/io/_plugins/tifffile_plugin.ini b/skimage/io/_plugins/tifffile_plugin.ini new file mode 100644 index 00000000..bf83fce2 --- /dev/null +++ b/skimage/io/_plugins/tifffile_plugin.ini @@ -0,0 +1,3 @@ +[tifffile] +description = Load and save TIFF and TIFF-based images using tifffile.py +provides = imread, imsave diff --git a/skimage/io/_plugins/tifffile_plugin.py b/skimage/io/_plugins/tifffile_plugin.py new file mode 100644 index 00000000..58038db4 --- /dev/null +++ b/skimage/io/_plugins/tifffile_plugin.py @@ -0,0 +1,6 @@ +try: + from tifffile import imread, imsave +except ImportError: + raise ImportError("The tifffile module could not be found.\n" + "It can be obtained at " + "\n") From ee755c6c0b58b4669412b8cfd007a31fe8d4b677 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 7 Oct 2014 07:02:01 -0500 Subject: [PATCH 0509/1122] Remove temporary file and clean up imports --- skimage/_shared/testing.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/skimage/_shared/testing.py b/skimage/_shared/testing.py index be58540a..510bf03d 100644 --- a/skimage/_shared/testing.py +++ b/skimage/_shared/testing.py @@ -1,11 +1,12 @@ """Testing utilities.""" +import os import re from tempfile import NamedTemporaryFile from skimage import ( - data, io, img_as_uint, img_as_bool, img_as_float, img_as_int, img_as_ubyte) + data, io, img_as_uint, img_as_float, img_as_int, img_as_ubyte) from numpy import testing import numpy as np @@ -91,6 +92,10 @@ def roundtrip(img, plugin, suffix): temp_file.close() fname = temp_file.name io.imsave(fname, img, plugin=plugin) + try: + os.remove(fname) + except Exception: + pass return io.imread(fname, plugin=plugin) From 8396d59268834eda91ce74ec129a1882386f7754 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 7 Oct 2014 07:13:43 -0500 Subject: [PATCH 0510/1122] Reinstate tifffile test --- skimage/io/tests/test_tifffile.py | 62 +++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 skimage/io/tests/test_tifffile.py diff --git a/skimage/io/tests/test_tifffile.py b/skimage/io/tests/test_tifffile.py new file mode 100644 index 00000000..7019f0c6 --- /dev/null +++ b/skimage/io/tests/test_tifffile.py @@ -0,0 +1,62 @@ +import os +import skimage as si +import skimage.io as sio +import numpy as np + +from numpy.testing import * +from numpy.testing.decorators import skipif +from tempfile import NamedTemporaryFile + +try: + import skimage.io._plugins.tifffile_plugin as tf + _plugins = sio.plugin_order() + TF_available = True + sio.use_plugin('tifffile') +except ImportError: + TF_available = False + +np.random.seed(0) + + +def teardown(): + sio.reset_plugins() + + +@skipif(not TF_available) +def test_imread_uint16(): + expected = np.load(os.path.join(si.data_dir, 'chessboard_GRAY_U8.npy')) + img = sio.imread(os.path.join(si.data_dir, 'chessboard_GRAY_U16.tif')) + assert img.dtype == np.uint16 + assert_array_almost_equal(img, expected) + + +@skipif(not TF_available) +def test_imread_uint16_big_endian(): + expected = np.load(os.path.join(si.data_dir, 'chessboard_GRAY_U8.npy')) + img = sio.imread(os.path.join(si.data_dir, 'chessboard_GRAY_U16B.tif')) + assert img.dtype == np.uint16 + assert_array_almost_equal(img, expected) + + +class TestSave: + def roundtrip(self, dtype, x): + f = NamedTemporaryFile(suffix='.tif') + fname = f.name + f.close() + sio.imsave(fname, x) + y = sio.imread(fname) + assert_array_equal(x, y) + + @skipif(not TF_available) + def test_imsave_roundtrip(self): + for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: + for dtype in (np.uint8, np.uint16, np.float32, np.float64): + x = np.ones(shape, dtype=dtype) * np.random.rand(*shape) + + if not np.issubdtype(dtype, float): + x = (x * 255).astype(dtype) + yield self.roundtrip, dtype, x + + +if __name__ == "__main__": + run_module_suite() From 74c87b583df8c993c5cd5c19e08efd3ced9b516c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 8 Oct 2014 20:35:24 -0500 Subject: [PATCH 0511/1122] Clean up imsave behavior and beef up docstring --- skimage/io/_plugins/pil_plugin.py | 97 ++++++++++++++++++------------- 1 file changed, 58 insertions(+), 39 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 33ac5bf0..627ed6cd 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -27,7 +27,10 @@ def imread(fname, dtype=None): dtype : numpy dtype object or string specifier Specifies data type of array elements. - + Notes + ----- + Tiff files are handled by Christophe Golhke's tifffile.py, and support many + advanced image types including multi-page and floating point. """ if hasattr(fname, 'lower') and dtype is None: if fname.lower().endswith(('.tiff', '.tif')): @@ -109,36 +112,43 @@ def ndarray_to_pil(arr, format_str=None): arr = img_as_ubyte(arr) mode = {3: 'RGB', 4: 'RGBA'}[arr.shape[2]] - elif arr.dtype.kind == 'f': - arr = img_as_uint(arr) - mode = 'I;16' - mode_base = 'I' + elif format_str.lower() == 'png': + if arr.dtype.kind == 'f': + arr = img_as_uint(arr) + mode = 'I;16' + mode_base = 'I' - elif arr.max() < 256 and arr.min() >= 0: - arr = arr.astype(np.uint8) - mode = 'L' - mode_base = 'L' + elif arr.max() < 256 and arr.min() >= 0: + arr = arr.astype(np.uint8) + mode = 'L' + mode_base = 'L' - elif arr.max() < 2**16 and arr.min() >= 0: - arr = arr.astype(np.uint16) - mode = 'I;16' - mode_base = 'I' + elif arr.min() >= 0: + arr = img_as_uint(arr) + mode = 'I;16' + mode_base = 'I' + + else: + arr = img_as_int(arr) + mode = 'I' + mode_base = 'I' else: - arr = img_as_int(arr) - mode = 'I' - mode_base = 'I' + arr = img_as_ubyte(arr) + mode = 'L' + mode_base = 'L' if arr.ndim == 2: im = Image.new(mode_base, arr.T.shape) im.fromstring(arr.tostring(), 'raw', mode) + else: - try: - im = Image.frombytes(mode, (arr.shape[1], arr.shape[0]), - arr.tostring()) - except AttributeError: - im = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), - arr.tostring()) + try: + im = Image.frombytes(mode, (arr.shape[1], arr.shape[0]), + arr.tostring()) + except AttributeError: + im = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), + arr.tostring()) return im @@ -159,28 +169,27 @@ def imsave(fname, arr, format_str=None): Notes ----- - Currently, only 8-bit precision is supported for PNG and JPEG. - Images that are not uint8 type will be converted using - `skimage.img_as_ubyte`. - 2D TIFF Files also support unit16 and float32 type images. + Tiff files are handled by Christophe Golhke's tifffile.py, and support many + advanced image types including multi-page and floating point. + + All other image formats use the Python Imaging Libary. + See http://pillow.readthedocs.org/en/latest/handbook/image-file-formats.html + for a list of other supported formats. Integer type images are only supported for PNGs. + All images besides single channel PNGs are converted using `img_as_uint8`. + Single Channel PNGS have the following behavior: + - Boolean types -> convert to uint8 + - Integer values in {0, 256} -> convert to uin8 + - Integer values > 0 -> convert to uint16 + - Integer values < 0 -> convert to int16 + - Floating point images -> convert to uint16 """ # default to PNG if file-like object if not isinstance(fname, string_types) and format_str is None: format_str = "PNG" - if arr.ndim not in (2, 3): - raise ValueError("Invalid shape for image array: %s" % arr.shape) - - if arr.ndim == 3: - if arr.shape[2] not in (3, 4): - raise ValueError("Invalid number of channels in image array.") - arr = np.asanyarray(arr).squeeze() - if arr.dtype.kind == 'b': - arr = arr.astype(np.uint8) - use_tif = False if hasattr(fname, 'lower'): if fname.lower().endswith(('.tiff', '.tif')): @@ -191,10 +200,20 @@ def imsave(fname, arr, format_str=None): if use_tif: tif_imsave(fname, arr) + return - else: - img = ndarray_to_pil(arr, format_str=None) - img.save(fname, format=format_str) + if arr.ndim not in (2, 3): + raise ValueError("Invalid shape for image array: %s" % arr.shape) + + if arr.ndim == 3: + if arr.shape[2] not in (3, 4): + raise ValueError("Invalid number of channels in image array.") + + if arr.dtype.kind == 'b': + arr = arr.astype(np.uint8) + + img = ndarray_to_pil(arr, format_str=None) + img.save(fname, format=format_str) def imshow(arr): From 9b768dc681f3ec7482e6354fac489ed6112b95f7 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 8 Oct 2014 21:31:42 -0500 Subject: [PATCH 0512/1122] Add references in docstrings --- skimage/io/_plugins/pil_plugin.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 627ed6cd..f7a1fb8b 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -29,8 +29,17 @@ def imread(fname, dtype=None): Notes ----- - Tiff files are handled by Christophe Golhke's tifffile.py, and support many + 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. + + References + ---------- + .. [1] http://www.lfd.uci.edu/~gohlke/code/tifffile.py.html + .. [2] http://pillow.readthedocs.org/en/latest/handbook/image-file-formats.html + """ if hasattr(fname, 'lower') and dtype is None: if fname.lower().endswith(('.tiff', '.tif')): @@ -112,7 +121,7 @@ def ndarray_to_pil(arr, format_str=None): arr = img_as_ubyte(arr) mode = {3: 'RGB', 4: 'RGBA'}[arr.shape[2]] - elif format_str.lower() == 'png': + elif format_str in ['png', 'PNG']: if arr.dtype.kind == 'f': arr = img_as_uint(arr) mode = 'I;16' @@ -169,12 +178,12 @@ def imsave(fname, arr, format_str=None): Notes ----- - Tiff files are handled by Christophe Golhke's tifffile.py, and support many + 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 http://pillow.readthedocs.org/en/latest/handbook/image-file-formats.html - for a list of other supported formats. Integer type images are only supported for PNGs. + See PIL docs [1] for a list of other supported formats. + Integer type images are only supported for PNGs. All images besides single channel PNGs are converted using `img_as_uint8`. Single Channel PNGS have the following behavior: - Boolean types -> convert to uint8 @@ -183,6 +192,11 @@ def imsave(fname, arr, format_str=None): - Integer values < 0 -> convert to int16 - Floating point images -> convert to uint16 + + References + ---------- + .. [1] http://www.lfd.uci.edu/~gohlke/code/tifffile.py.html + .. [2] 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: From fb9f7e734663661c048c045b62a6ae296c84ac96 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 8 Oct 2014 22:06:09 -0500 Subject: [PATCH 0513/1122] Properly handle all image types for PNG, beef up docs --- skimage/io/_plugins/pil_plugin.py | 53 +++++++++++++++---------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index f7a1fb8b..19388a2e 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -32,9 +32,9 @@ def imread(fname, dtype=None): 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. + All other files are read using the Python Imaging Libary. See PIL docs [2] for a list of supported formats. - + References ---------- .. [1] http://www.lfd.uci.edu/~gohlke/code/tifffile.py.html @@ -122,25 +122,21 @@ def ndarray_to_pil(arr, format_str=None): mode = {3: 'RGB', 4: 'RGBA'}[arr.shape[2]] elif format_str in ['png', 'PNG']: + mode = 'I;16' + mode_base = 'I' + if arr.dtype.kind == 'f': arr = img_as_uint(arr) - mode = 'I;16' - mode_base = 'I' elif arr.max() < 256 and arr.min() >= 0: arr = arr.astype(np.uint8) - mode = 'L' - mode_base = 'L' - - elif arr.min() >= 0: - arr = img_as_uint(arr) - mode = 'I;16' - mode_base = 'I' + mode = mode_base = 'L' else: - arr = img_as_int(arr) - mode = 'I' - mode_base = 'I' + if arr.dtype.kind == 'u': + arr = img_as_uint(arr) + else: + arr = img_as_int(arr) else: arr = img_as_ubyte(arr) @@ -178,20 +174,17 @@ def imsave(fname, arr, format_str=None): Notes ----- - Tiff files are handled by Christophe Golhke's tifffile.py [1], and support many - advanced image types including multi-page and floating point. + 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 [1] for a list of other supported formats. - Integer type images are only supported for PNGs. + 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: - - Boolean types -> convert to uint8 - - Integer values in {0, 256} -> convert to uin8 - - Integer values > 0 -> convert to uint16 - - Integer values < 0 -> convert to int16 - - Floating point images -> convert to uint16 - + - Integer values in [0, 255] and Boolean types -> img_as_uint8 + - Other unsigned integers and floating point -> img_as_uint16 + - Other signed integers -> img_as_int16 References ---------- @@ -201,9 +194,16 @@ def imsave(fname, arr, format_str=None): # default to PNG if file-like object if not isinstance(fname, string_types) and format_str is None: format_str = "PNG" + # Check for png in filename + if (isinstance(fname, string_types) + and fname.lower().endswith(".png")): + format_str = "PNG" arr = np.asanyarray(arr).squeeze() + if arr.dtype.kind == 'b': + arr = arr.astype(np.uint8) + use_tif = False if hasattr(fname, 'lower'): if fname.lower().endswith(('.tiff', '.tif')): @@ -223,10 +223,7 @@ def imsave(fname, arr, format_str=None): if arr.shape[2] not in (3, 4): raise ValueError("Invalid number of channels in image array.") - if arr.dtype.kind == 'b': - arr = arr.astype(np.uint8) - - img = ndarray_to_pil(arr, format_str=None) + img = ndarray_to_pil(arr, format_str=format_str) img.save(fname, format=format_str) From be279de2fa5ffab2997667ce1bf96a7cdf99217b Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 8 Oct 2014 22:06:45 -0500 Subject: [PATCH 0514/1122] Fix handling of temp file removal --- skimage/_shared/testing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/_shared/testing.py b/skimage/_shared/testing.py index 510bf03d..0d40c47f 100644 --- a/skimage/_shared/testing.py +++ b/skimage/_shared/testing.py @@ -92,11 +92,12 @@ def roundtrip(img, plugin, suffix): temp_file.close() fname = temp_file.name io.imsave(fname, img, plugin=plugin) + new = io.imread(fname, plugin=plugin) try: os.remove(fname) except Exception: pass - return io.imread(fname, plugin=plugin) + return new def ubyte_check(plugin, fmt='png'): From 047650ce3822fd695c3acfdbe8be6a8b2b85b0a9 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 8 Oct 2014 22:12:34 -0500 Subject: [PATCH 0515/1122] Fix reference citation --- skimage/io/_plugins/pil_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 19388a2e..decf0281 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -179,7 +179,7 @@ def imsave(fname, arr, format_str=None): floating point. All other image formats use the Python Imaging Libary. - See PIL docs [1] for a list of other supported formats. + See PIL docs [2] 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 From bae672f34b41da4ac596e879d02eec2ebbfcc874 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 8 Oct 2014 22:42:51 -0500 Subject: [PATCH 0516/1122] Do not try and save signed integer output, improved signed checks for tif. --- skimage/_shared/testing.py | 18 ++++++++++++++---- skimage/io/_plugins/pil_plugin.py | 10 ++++------ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/skimage/_shared/testing.py b/skimage/_shared/testing.py index 0d40c47f..db3bcfe8 100644 --- a/skimage/_shared/testing.py +++ b/skimage/_shared/testing.py @@ -119,8 +119,13 @@ def ubyte_check(plugin, fmt='png'): testing.assert_allclose(r3, img) img4 = img_as_int(img) - r4 = roundtrip(img4, plugin, fmt) - testing.assert_allclose(r4, img) + if fmt.lower() in (('tif', 'tiff')): + img4 -= 100 + r4 = roundtrip(img4, plugin, fmt) + testing.assert_allclose(r4, img4) + else: + r4 = roundtrip(img4, plugin, fmt) + testing.assert_allclose(r4, img_as_ubyte(img4)) img5 = img_as_uint(img) r5 = roundtrip(img5, plugin, fmt) @@ -150,8 +155,13 @@ def full_range_check(plugin, fmt='png'): testing.assert_allclose(r3, img_as_uint(img)) img4 = img_as_int(img) - r4 = roundtrip(img4, plugin, fmt) - testing.assert_allclose(r4, img4) + if fmt.lower() in (('tif', 'tiff')): + img4 -= 100 + r4 = roundtrip(img4, plugin, fmt) + testing.assert_allclose(r4, img4) + else: + r4 = roundtrip(img4, plugin, fmt) + testing.assert_allclose(r4, img_as_uint(img4)) img5 = img_as_uint(img) r5 = roundtrip(img5, plugin, fmt) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index decf0281..a692da54 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -75,6 +75,8 @@ def pil_to_ndarray(im, dtype=None): elif im.mode.startswith('I;16'): shape = im.size dtype = '>u2' if im.mode.endswith('B') else ' img_as_uint8 - - Other unsigned integers and floating point -> img_as_uint16 - - Other signed integers -> img_as_int16 + - Floating point and other integers -> img_as_uint16 References ---------- From 708ab0593bb2747c868b8cc35e8d5b8f1b9f04f4 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 8 Oct 2014 22:44:15 -0500 Subject: [PATCH 0517/1122] Fix docstring typo --- skimage/io/_plugins/pil_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index a692da54..05d315bd 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -180,7 +180,7 @@ def imsave(fname, arr, format_str=None): All other image formats use the Python Imaging Libary. See PIL docs [2] 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: + Single Channel PNGs have the following behavior: - Integer values in [0, 255] and Boolean types -> img_as_uint8 - Floating point and other integers -> img_as_uint16 From a623fd3e44d59dba4e9f7fc8cba2505edc9c167b Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 8 Oct 2014 22:58:31 -0500 Subject: [PATCH 0518/1122] Rename the convenience methods --- skimage/_shared/testing.py | 17 ++++++++--------- skimage/io/tests/test_pil.py | 10 +++++----- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/skimage/_shared/testing.py b/skimage/_shared/testing.py index db3bcfe8..f8c7a88e 100644 --- a/skimage/_shared/testing.py +++ b/skimage/_shared/testing.py @@ -100,8 +100,8 @@ def roundtrip(img, plugin, suffix): return new -def ubyte_check(plugin, fmt='png'): - """Check roundtrip behavior for images that can only be saved as uint8 +def color_check(plugin, fmt='png'): + """Check roundtrip behavior for color images. All major input types should be handled as ubytes and read back correctly. @@ -132,11 +132,10 @@ def ubyte_check(plugin, fmt='png'): testing.assert_allclose(r5, img) -def full_range_check(plugin, fmt='png'): +def mono_check(plugin, fmt='png'): """Check the roundtrip behavior for images that support most types. - All major input types should be handled, except bool is treated - as ubyte and float can treated as uint16 or float. + All major input types should be handled. """ img = img_as_ubyte(data.moon()) @@ -169,7 +168,7 @@ def full_range_check(plugin, fmt='png'): if __name__ == '__main__': - ubyte_check('pil') - full_range_check('pil') - ubyte_check('pil', 'bmp') - full_range_check('pil', 'tiff') + color_check('pil') + mono_check('pil') + mono_check('pil', 'bmp') + mono_check('pil', 'tiff') diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index 7b5cdd49..ee9add79 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -8,7 +8,7 @@ from tempfile import NamedTemporaryFile from skimage import data_dir from skimage.io import (imread, imsave, use_plugin, reset_plugins, Image as ioImage) -from skimage._shared.testing import ubyte_check, full_range_check +from skimage._shared.testing import mono_check, color_check from six import BytesIO @@ -157,13 +157,13 @@ def test_imexport_imimport(): def test_all_color(): - ubyte_check('pil') - ubyte_check('pil', 'bmp') + color_check('pil') + color_check('pil', 'bmp') def test_all_mono(): - full_range_check('pil') - full_range_check('pil', 'tiff') + mono_check('pil') + mono_check('pil', 'tiff') class TestSaveTIF: From 3b3b44b1279d5127585d3f5fdd255e6b0a316bdb Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 05:54:14 -0500 Subject: [PATCH 0519/1122] Clean up pil and tif tests --- skimage/io/tests/test_pil.py | 45 +++++++++++++++++-------------- skimage/io/tests/test_tifffile.py | 33 ++++++++++------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index ee9add79..8e2c8b28 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -1,7 +1,6 @@ import os.path import numpy as np -from numpy.testing import * -from numpy.testing.decorators import skipif +import numpy.testing as npt from tempfile import NamedTemporaryFile @@ -13,7 +12,8 @@ from skimage._shared.testing import mono_check, color_check from six import BytesIO from PIL import Image -from skimage.io._plugins.pil_plugin import pil_to_ndarray, ndarray_to_pil, _palette_is_grayscale +from skimage.io._plugins.pil_plugin import ( + pil_to_ndarray, ndarray_to_pil, _palette_is_grayscale) use_plugin('pil') np.random.seed(0) @@ -63,14 +63,14 @@ def test_bilevel(): expected[::2] = 255 img = imread(os.path.join(data_dir, 'checker_bilevel.png')) - assert_array_equal(img, expected) + npt.assert_array_equal(img, expected) def test_imread_uint16(): expected = np.load(os.path.join(data_dir, 'chessboard_GRAY_U8.npy')) img = imread(os.path.join(data_dir, 'chessboard_GRAY_U16.tif')) assert np.issubdtype(img.dtype, np.uint16) - assert_array_almost_equal(img, expected) + npt.assert_array_almost_equal(img, expected) def test_repr_png(): @@ -87,15 +87,15 @@ def test_repr_png(): def test_imread_truncated_jpg(): - assert_raises((IOError, ValueError), imread, - os.path.join(data_dir, 'truncated.jpg')) + npt.assert_raises((IOError, ValueError), imread, + os.path.join(data_dir, 'truncated.jpg')) def test_imread_uint16_big_endian(): expected = np.load(os.path.join(data_dir, 'chessboard_GRAY_U8.npy')) img = imread(os.path.join(data_dir, 'chessboard_GRAY_U16B.tif')) assert img.dtype == np.uint16 - assert_array_almost_equal(img, expected) + npt.assert_array_almost_equal(img, expected) class TestSave: @@ -113,7 +113,7 @@ class TestSave: return y def verify_roundtrip(self, dtype, x, y, scaling=1): - assert_array_almost_equal((x * scaling).astype(np.int32), y) + npt.assert_array_almost_equal((x * scaling).astype(np.int32), y) def verify_imsave_roundtrip(self, roundtrip_function): for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: @@ -121,16 +121,18 @@ class TestSave: x = np.ones(shape, dtype=dtype) * np.random.rand(*shape) if np.issubdtype(dtype, float): - yield self.verify_roundtrip, dtype, x, roundtrip_function(x), 255 + yield (self.verify_roundtrip, dtype, x, + roundtrip_function(x), 255) else: x = (x * 255).astype(dtype) - yield self.verify_roundtrip, dtype, x, roundtrip_function(x) + yield (self.verify_roundtrip, dtype, x, + roundtrip_function(x)) def test_imsave_roundtrip_file(self): - self.verify_imsave_roundtrip(self.roundtrip_file) + self.verify_imsave_roundtrip(self.roundtrip_file) def test_imsave_roundtrip_pil_image(self): - self.verify_imsave_roundtrip(self.roundtrip_pil_image) + self.verify_imsave_roundtrip(self.roundtrip_pil_image) def test_imsave_filelike(): @@ -145,7 +147,7 @@ def test_imsave_filelike(): s.seek(0) out = imread(s) assert out.shape == shape - assert_allclose(out, image) + npt.assert_allclose(out, image) def test_imexport_imimport(): @@ -173,16 +175,19 @@ class TestSaveTIF: f.close() imsave(fname, x) y = imread(fname) - assert_array_equal(x, y) + npt.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.float32, np.float64): - x = np.ones(shape, dtype=dtype) * np.random.rand(*shape) + for dtype in (np.uint8, np.uint16, np.int16, np.float32, + np.float64, np.bool): + x = np.random.rand(*shape) - if not np.issubdtype(dtype, float): - x = (x * 255).astype(dtype) + 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 if __name__ == "__main__": - run_module_suite() + npt.run_module_suite() diff --git a/skimage/io/tests/test_tifffile.py b/skimage/io/tests/test_tifffile.py index 7019f0c6..41da753f 100644 --- a/skimage/io/tests/test_tifffile.py +++ b/skimage/io/tests/test_tifffile.py @@ -3,17 +3,12 @@ import skimage as si import skimage.io as sio import numpy as np -from numpy.testing import * -from numpy.testing.decorators import skipif +import numpy.testing as npt from tempfile import NamedTemporaryFile -try: - import skimage.io._plugins.tifffile_plugin as tf - _plugins = sio.plugin_order() - TF_available = True - sio.use_plugin('tifffile') -except ImportError: - TF_available = False +_plugins = sio.plugin_order() +sio.use_plugin('tifffile') + np.random.seed(0) @@ -22,20 +17,18 @@ def teardown(): sio.reset_plugins() -@skipif(not TF_available) def test_imread_uint16(): expected = np.load(os.path.join(si.data_dir, 'chessboard_GRAY_U8.npy')) img = sio.imread(os.path.join(si.data_dir, 'chessboard_GRAY_U16.tif')) assert img.dtype == np.uint16 - assert_array_almost_equal(img, expected) + npt.assert_array_almost_equal(img, expected) -@skipif(not TF_available) def test_imread_uint16_big_endian(): expected = np.load(os.path.join(si.data_dir, 'chessboard_GRAY_U8.npy')) img = sio.imread(os.path.join(si.data_dir, 'chessboard_GRAY_U16B.tif')) assert img.dtype == np.uint16 - assert_array_almost_equal(img, expected) + npt.assert_array_almost_equal(img, expected) class TestSave: @@ -45,18 +38,20 @@ class TestSave: f.close() sio.imsave(fname, x) y = sio.imread(fname) - assert_array_equal(x, y) + npt.assert_array_equal(x, y) - @skipif(not TF_available) def test_imsave_roundtrip(self): for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: - for dtype in (np.uint8, np.uint16, np.float32, np.float64): - x = np.ones(shape, dtype=dtype) * np.random.rand(*shape) + for dtype in (np.uint8, np.uint16, np.float32, np.int16, + np.float64): + x = np.random.rand(*shape) if not np.issubdtype(dtype, float): - x = (x * 255).astype(dtype) + x = (x * np.iinfo(dtype).max).astype(dtype) + else: + x = x.astype(dtype) yield self.roundtrip, dtype, x if __name__ == "__main__": - run_module_suite() + npt.run_module_suite() From ae195781925831e3b282f0b7dc3d853cff664aa5 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 05:56:17 -0500 Subject: [PATCH 0520/1122] Remove unused import --- skimage/io/_plugins/pil_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 05d315bd..78afa05c 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -11,7 +11,7 @@ except ImportError: "http://pypi.python.org/pypi/PIL/) " "for further instructions.") -from skimage.util import img_as_ubyte, img_as_uint, img_as_int +from skimage.util import img_as_ubyte, img_as_uint from six import string_types from .tifffile import imread as tif_imread, imsave as tif_imsave From ba730091a09defb7182a20afda2a24c9ad768507 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 19:23:15 -0500 Subject: [PATCH 0521/1122] Fix relative import and test import convention --- skimage/io/_plugins/tifffile_plugin.py | 2 +- skimage/io/tests/test_pil.py | 22 ++++++++++++---------- skimage/io/tests/test_tifffile.py | 12 +++++++----- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/skimage/io/_plugins/tifffile_plugin.py b/skimage/io/_plugins/tifffile_plugin.py index 58038db4..fb0494c4 100644 --- a/skimage/io/_plugins/tifffile_plugin.py +++ b/skimage/io/_plugins/tifffile_plugin.py @@ -1,5 +1,5 @@ try: - from tifffile import imread, imsave + from .tifffile import imread, imsave except ImportError: raise ImportError("The tifffile module could not be found.\n" "It can be obtained at " diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index 8e2c8b28..02b3a4b8 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -1,6 +1,8 @@ import os.path import numpy as np -import numpy.testing as npt +from numpy.testing import ( + assert_array_equal, assert_array_almost_equal, assert_raises, + assert_allclose, run_module_suite) from tempfile import NamedTemporaryFile @@ -63,14 +65,14 @@ def test_bilevel(): expected[::2] = 255 img = imread(os.path.join(data_dir, 'checker_bilevel.png')) - npt.assert_array_equal(img, expected) + assert_array_equal(img, expected) def test_imread_uint16(): expected = np.load(os.path.join(data_dir, 'chessboard_GRAY_U8.npy')) img = imread(os.path.join(data_dir, 'chessboard_GRAY_U16.tif')) assert np.issubdtype(img.dtype, np.uint16) - npt.assert_array_almost_equal(img, expected) + assert_array_almost_equal(img, expected) def test_repr_png(): @@ -87,15 +89,15 @@ def test_repr_png(): def test_imread_truncated_jpg(): - npt.assert_raises((IOError, ValueError), imread, - os.path.join(data_dir, 'truncated.jpg')) + assert_raises((IOError, ValueError), imread, + os.path.join(data_dir, 'truncated.jpg')) def test_imread_uint16_big_endian(): expected = np.load(os.path.join(data_dir, 'chessboard_GRAY_U8.npy')) img = imread(os.path.join(data_dir, 'chessboard_GRAY_U16B.tif')) assert img.dtype == np.uint16 - npt.assert_array_almost_equal(img, expected) + assert_array_almost_equal(img, expected) class TestSave: @@ -113,7 +115,7 @@ class TestSave: return y def verify_roundtrip(self, dtype, x, y, scaling=1): - npt.assert_array_almost_equal((x * scaling).astype(np.int32), y) + assert_array_almost_equal((x * scaling).astype(np.int32), y) def verify_imsave_roundtrip(self, roundtrip_function): for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: @@ -147,7 +149,7 @@ def test_imsave_filelike(): s.seek(0) out = imread(s) assert out.shape == shape - npt.assert_allclose(out, image) + assert_allclose(out, image) def test_imexport_imimport(): @@ -175,7 +177,7 @@ class TestSaveTIF: f.close() imsave(fname, x) y = imread(fname) - npt.assert_array_equal(x, y) + assert_array_equal(x, y) def test_imsave_roundtrip(self): for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: @@ -190,4 +192,4 @@ class TestSaveTIF: yield self.roundtrip, dtype, x if __name__ == "__main__": - npt.run_module_suite() + run_module_suite() diff --git a/skimage/io/tests/test_tifffile.py b/skimage/io/tests/test_tifffile.py index 41da753f..fc958145 100644 --- a/skimage/io/tests/test_tifffile.py +++ b/skimage/io/tests/test_tifffile.py @@ -3,7 +3,9 @@ import skimage as si import skimage.io as sio import numpy as np -import numpy.testing as npt +from numpy.testing import ( + assert_array_equal, assert_array_almost_equal, run_module_suite) + from tempfile import NamedTemporaryFile _plugins = sio.plugin_order() @@ -21,14 +23,14 @@ def test_imread_uint16(): expected = np.load(os.path.join(si.data_dir, 'chessboard_GRAY_U8.npy')) img = sio.imread(os.path.join(si.data_dir, 'chessboard_GRAY_U16.tif')) assert img.dtype == np.uint16 - npt.assert_array_almost_equal(img, expected) + assert_array_almost_equal(img, expected) def test_imread_uint16_big_endian(): expected = np.load(os.path.join(si.data_dir, 'chessboard_GRAY_U8.npy')) img = sio.imread(os.path.join(si.data_dir, 'chessboard_GRAY_U16B.tif')) assert img.dtype == np.uint16 - npt.assert_array_almost_equal(img, expected) + assert_array_almost_equal(img, expected) class TestSave: @@ -38,7 +40,7 @@ class TestSave: f.close() sio.imsave(fname, x) y = sio.imread(fname) - npt.assert_array_equal(x, y) + assert_array_equal(x, y) def test_imsave_roundtrip(self): for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]: @@ -54,4 +56,4 @@ class TestSave: if __name__ == "__main__": - npt.run_module_suite() + run_module_suite() From b51d95f3e2f234f1a32b109b0629c4d0a5515298 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 20:47:12 -0500 Subject: [PATCH 0522/1122] Try removing the system numpy --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 9325de32..dda55455 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,7 @@ before_install: - sudo apt-get update - travis_retry pip install wheel flake8 coveralls nose + - travis_retry pip uninstall numpy # on Python 2.7, use the system versions of numpy, scipy, and matplotlib # and the minimum version of cython and networkx From 4c908d467b430a29075550c1c64fee7547f0476c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 21:17:50 -0500 Subject: [PATCH 0523/1122] Force numpy uninstall --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index dda55455..bdfb8566 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ before_install: - sudo apt-get update - travis_retry pip install wheel flake8 coveralls nose - - travis_retry pip uninstall numpy + - travis_retry pip uninstall -y numpy # on Python 2.7, use the system versions of numpy, scipy, and matplotlib # and the minimum version of cython and networkx From a618ad07736e22ff2bc7d35ae8cf038bf3661585 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 21:26:59 -0500 Subject: [PATCH 0524/1122] Make sure there is no previous matplotlib or scipy --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bdfb8566..78653fa8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ before_install: - sudo apt-get update - travis_retry pip install wheel flake8 coveralls nose - - travis_retry pip uninstall -y numpy + - travis_retry pip uninstall -y numpy matplotlib scipy # on Python 2.7, use the system versions of numpy, scipy, and matplotlib # and the minimum version of cython and networkx From 78453cb4d7fde7e899997af85ec996f9104a7977 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 21:51:37 -0500 Subject: [PATCH 0525/1122] Numpy is the only one installed --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 78653fa8..a59390c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ before_install: - sudo apt-get update - travis_retry pip install wheel flake8 coveralls nose - - travis_retry pip uninstall -y numpy matplotlib scipy + - travis_retry pip uninstall -y numpy # on Python 2.7, use the system versions of numpy, scipy, and matplotlib # and the minimum version of cython and networkx @@ -33,7 +33,7 @@ before_install: pip install https://github.com/networkx/networkx/archive/networkx-1.8.tar.gz; fi - - travis_retry pip install -r requirements.txt $WHEELHOUSE; + - travis_retry pip install -r requirements.txt $WHEELHOUSE; - python check_bento_build.py install: From fe6912b5914d5150c5770e394ad258e21edb4036 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 05:43:46 -0500 Subject: [PATCH 0526/1122] Add 'imsave' to the '*' output of pil_plugin --- skimage/io/_plugins/pil_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 78afa05c..123d27ea 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -1,4 +1,4 @@ -__all__ = ['imread'] +__all__ = ['imread', 'imsave'] import numpy as np From 9baa6c7f2daf9e01d141bf3064719776264bad47 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 06:26:30 -0500 Subject: [PATCH 0527/1122] Update doc / trigger a travis rebuild --- doc/travis_notes.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/travis_notes.txt b/doc/travis_notes.txt index 787e7c11..09d04889 100644 --- a/doc/travis_notes.txt +++ b/doc/travis_notes.txt @@ -27,4 +27,4 @@ failing (useful for installing from third party sources) - Feel free to cancel a build rather than waiting for it to go to completion if you have made a change to that branch. -- A VM with 64bit Ubuntu 12.04 is a huge help. +- A VM with 64bit Ubuntu 12.04 is a huge help for debugging. From b48c82f840a44bc27cfb03ae196b6b3dc447cdae Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 20:17:00 -0500 Subject: [PATCH 0528/1122] Make pil the default plugin --- skimage/io/manage_plugins.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/skimage/io/manage_plugins.py b/skimage/io/manage_plugins.py index d31e6dbe..c9e96f8e 100644 --- a/skimage/io/manage_plugins.py +++ b/skimage/io/manage_plugins.py @@ -44,10 +44,7 @@ plugin_meta_data = {} # the following preferences. preferred_plugins = { # Default plugins for all types (overridden by specific types below). - 'all': ['matplotlib', 'pil', 'qt', 'freeimage', 'null'], - # Use PIL as the default imread plugin, since matplotlib (1.2.x) - # is buggy (flips PNGs around, returns bytes as floats, etc.) - 'imread': ['pil'], + 'all': ['pil', 'matplotlib', 'qt', 'freeimage', 'null'] } From 84a56fa4a0fbce969be1a6c7534ac2fef4536dea Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 11 Oct 2014 04:48:18 -0500 Subject: [PATCH 0529/1122] Fix docstring links --- skimage/io/_plugins/pil_plugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 123d27ea..a23050db 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -29,11 +29,11 @@ def imread(fname, dtype=None): Notes ----- - Tiff files are handled by Christophe Golhke's tifffile.py [1], and support many + 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. + See PIL docs [2]_ for a list of supported formats. References ---------- @@ -173,12 +173,12 @@ def imsave(fname, arr, format_str=None): Notes ----- - Tiff files are handled by Christophe Golhke's tifffile.py [1], + 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. + See PIL docs [2]_ 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 From cf7c8750b56173caeb7f8497ad53298704fd7e99 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 19:42:47 -0500 Subject: [PATCH 0530/1122] Push parts of travis into separate shell scripts --- .travis.yml | 75 ++----------------------------------------- doc/travis_notes.txt | 22 ++----------- tools/travis_build.sh | 59 ++++++++++++++++++++++++++++++++++ tools/travis_setup.sh | 12 +++++++ 4 files changed, 76 insertions(+), 92 deletions(-) create mode 100644 tools/travis_build.sh create mode 100644 tools/travis_setup.sh diff --git a/.travis.yml b/.travis.yml index a59390c2..122189e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,19 +22,7 @@ before_install: - sh -e /etc/init.d/xvfb start - sudo apt-get update - - travis_retry pip install wheel flake8 coveralls nose - - travis_retry pip uninstall -y numpy - - # on Python 2.7, use the system versions of numpy, scipy, and matplotlib - # and the minimum version of cython and networkx - - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then - travis_retry sudo apt-get install python-scipy python-matplotlib; - pip install https://github.com/cython/cython/archive/0.19.2.tar.gz; - pip install https://github.com/networkx/networkx/archive/networkx-1.8.tar.gz; - fi - - - travis_retry pip install -r requirements.txt $WHEELHOUSE; - - python check_bento_build.py + - tools/travis_setup.sh install: - tools/header.py "Dependency versions" @@ -43,64 +31,7 @@ install: - python setup.py build_ext --inplace script: - - tools/header.py "Run all tests with minimum dependencies" - - nosetests --exe -v skimage - - - tools/header.py "Pep8 and Flake tests" - - flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples - - - tools/header.py "Install optional dependencies" - - # Install Qt and then update the Matplotlib settings - - if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then - sudo apt-get install -q python-qt4; - export SCI_QT_API=PyQt4; - - else - sudo apt-get install -q libqt4-dev; - travis_retry pip install PySide $WHEELHOUSE; - python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install; - export SCI_QT_API=Pyside; - fi - - # Matplotlib settings - must be after we install Pyside - - export MPL_DIR=$HOME/.config/matplotlib - - mkdir -p $MPL_DIR - - touch $MPL_DIR/matplotlibrc - - "echo 'backend : Agg' > $MPL_DIR/matplotlibrc" - - "echo 'backend.qt4 : '$SCI_QT_API >> $MPL_DIR/matplotlibrc" - - # - imread does NOT support py3.2 - - if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then - sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools; - travis_retry pip install imread; - fi - - # TODO: update when SimpleITK become available on py34 or hopefully pip - - if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then - travis_retry easy_install SimpleITK; - fi - - - travis_retry sudo apt-get install libfreeimage3 - - travis_retry pip install astropy - - - if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then - travis_retry pip install pyamg; - fi - - - tools/header.py "Run doc examples" - - export PYTHONPATH=$(pwd):$PYTHONPATH - - for f in doc/examples/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done - - for f in doc/examples/applications/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done - - - tools/header.py "Run tests with all dependencies" - # run tests again with optional dependencies to get more coverage - # measure coverage on py3.3 - - if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then - nosetests --exe -v --with-doctest --with-cov --cover-package skimage; - else - nosetests --exe -v --with-doctest skimage; - fi + - tools/travis_build.sh after_success: - - coveralls; + - coveralls diff --git a/doc/travis_notes.txt b/doc/travis_notes.txt index 09d04889..c5d5c99c 100644 --- a/doc/travis_notes.txt +++ b/doc/travis_notes.txt @@ -3,26 +3,8 @@ http://lint.travis-ci.org/ is recommended elsewhere but does not give helpful error reports. - Make sure all of your "-" lines start on the same column -- Make sure all of your "if" lines are aligned with the "else" and "fi" lines -- Recommend using tab stops (but not tab chars) everywhere for alignment -- "If" blocks must be on one travis statement and have semicolons at the - end of each line: - -``` - - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then - echo "2.7"; - else - echo "Not 2.7"; - end -``` - -- "If" blocks cannot contain comments -- All travis commands are run with `eval` and quotes are taken as literal - characters unless you wrap the whole line in quotes: -`echo "hello : world"` is interpreted as `echo \"hello : world\"` -`"echo 'hello : world'"` is interpreted as `echo 'hello : world'` -`"echo 'hello : '$(MYVAR)'world'"` is interpreted as - `echo 'hello : $(MYVAR)world'` +- Use shell scripts for `before_install` and `script` or any part that + has conditional statements - Use `travis_retry` before a command to have it try several times before failing (useful for installing from third party sources) - Feel free to cancel a build rather than waiting for it to go to completion diff --git a/tools/travis_build.sh b/tools/travis_build.sh new file mode 100644 index 00000000..79b8f9b5 --- /dev/null +++ b/tools/travis_build.sh @@ -0,0 +1,59 @@ +./header.py "Run all tests with minimum dependencies" +nosetests --exe -v skimage + +./header.py "Pep8 and Flake tests" +flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples + +./header.py "Install optional dependencies" + +# Install Qt and then update the Matplotlib settings +if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then + sudo apt-get install -q python-qt4 + export SCI_QT_API=PyQt4 + +else + sudo apt-get install -q libqt4-dev + travis_retry pip install PySide $WHEELHOUSE + python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install + export SCI_QT_API=PySide +fi + +# Matplotlib settings - must be after we install Pyside +export MPL_DIR=$HOME/.config/matplotlib +mkdir -p $MPL_DIR +touch $MPL_DIR/matplotlibrc +echo 'backend : Agg' > $MPL_DIR/matplotlibrc +echo 'backend.qt4 : '$SCI_QT_API >> $MPL_DIR/matplotlibrc + +# imread does NOT support py3.2 +if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then + sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools + travis_retry pip install imread +fi + +# TODO: update when SimpleITK become available on py34 or hopefully pip +if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then + travis_retry easy_install SimpleITK +fi + +travis_retry sudo apt-get install libfreeimage3 +travis_retry pip install astropy + +if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then + travis_retry pip install pyamg +fi + +./header.py "Run doc examples" +export PYTHONPATH=$(pwd):$PYTHONPATH +for f in doc/examples/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done +for f in doc/examples/applications/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done + +./header.py "Run tests with all dependencies" +# run tests again with optional dependencies to get more coverage +# measure coverage on py3.3 +if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then + nosetests --exe -v --with-doctest --with-cov --cover-package skimage +else + nosetests --exe -v --with-doctest skimage +fi + diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh new file mode 100644 index 00000000..c6d8c566 --- /dev/null +++ b/tools/travis_setup.sh @@ -0,0 +1,12 @@ +travis_retry pip install wheel flake8 coveralls nose + +# on Python 2.7, use the system versions of numpy, scipy, and matplotlib +# and the minimum version of cython and networkx +if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then + travis_retry sudo apt-get install python-scipy python-matplotlib + pip install https://github.com/cython/cython/archive/0.19.2.tar.gz + pip install https://github.com/networkx/networkx/archive/networkx-1.8.tar.gz +fi + +travis_retry pip install -r requirements.txt $WHEELHOUSE +python check_bento_build.py From 4233e9fb3afde7acf9a7b28c98c30f69e923774f Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 19:45:30 -0500 Subject: [PATCH 0531/1122] Remove variable exports --- tools/travis_build.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/travis_build.sh b/tools/travis_build.sh index 79b8f9b5..f45875ae 100644 --- a/tools/travis_build.sh +++ b/tools/travis_build.sh @@ -9,17 +9,17 @@ flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples # Install Qt and then update the Matplotlib settings if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then sudo apt-get install -q python-qt4 - export SCI_QT_API=PyQt4 + SCI_QT_API=PyQt4 else sudo apt-get install -q libqt4-dev travis_retry pip install PySide $WHEELHOUSE python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install - export SCI_QT_API=PySide + SCI_QT_API=PySide fi # Matplotlib settings - must be after we install Pyside -export MPL_DIR=$HOME/.config/matplotlib +MPL_DIR=$HOME/.config/matplotlib mkdir -p $MPL_DIR touch $MPL_DIR/matplotlibrc echo 'backend : Agg' > $MPL_DIR/matplotlibrc @@ -44,7 +44,7 @@ if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then fi ./header.py "Run doc examples" -export PYTHONPATH=$(pwd):$PYTHONPATH +PYTHONPATH=$(pwd):$PYTHONPATH for f in doc/examples/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done for f in doc/examples/applications/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done From b1f7bd813312df45f1c8ef2280aaa3d58e3042e0 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 19:48:50 -0500 Subject: [PATCH 0532/1122] Add shebang lines --- tools/travis_build.sh | 1 + tools/travis_setup.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/tools/travis_build.sh b/tools/travis_build.sh index f45875ae..7ed37c56 100644 --- a/tools/travis_build.sh +++ b/tools/travis_build.sh @@ -1,3 +1,4 @@ +#!/usr/bin/sh ./header.py "Run all tests with minimum dependencies" nosetests --exe -v skimage diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh index c6d8c566..ad740a69 100644 --- a/tools/travis_setup.sh +++ b/tools/travis_setup.sh @@ -1,3 +1,4 @@ +#!/usr/bin/sh travis_retry pip install wheel flake8 coveralls nose # on Python 2.7, use the system versions of numpy, scipy, and matplotlib From 0bad613568bab7f39cc60fc3593dca976c6c1b76 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 19:51:00 -0500 Subject: [PATCH 0533/1122] Add explicit sh executable --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 122189e6..fbefad0b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ before_install: - sh -e /etc/init.d/xvfb start - sudo apt-get update - - tools/travis_setup.sh + - sh tools/travis_setup.sh install: - tools/header.py "Dependency versions" @@ -31,7 +31,7 @@ install: - python setup.py build_ext --inplace script: - - tools/travis_build.sh + - sh tools/travis_build.sh after_success: - coveralls From 87d3a07b71003f37ab31a22b9cc51e991e3999a9 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 20:01:43 -0500 Subject: [PATCH 0534/1122] Reorganize files --- .travis.yml | 25 +++++++++++++-- tools/travis_build.sh | 60 ------------------------------------ tools/travis_get_optional.sh | 39 +++++++++++++++++++++++ tools/travis_setup.sh | 6 ++-- 4 files changed, 65 insertions(+), 65 deletions(-) delete mode 100644 tools/travis_build.sh create mode 100644 tools/travis_get_optional.sh diff --git a/.travis.yml b/.travis.yml index fbefad0b..4a4288fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ before_install: - sh -e /etc/init.d/xvfb start - sudo apt-get update - - sh tools/travis_setup.sh + - travis_retry sh tools/travis_setup.sh install: - tools/header.py "Dependency versions" @@ -31,7 +31,28 @@ install: - python setup.py build_ext --inplace script: - - sh tools/travis_build.sh + - tools/header.py "Run all tests with minimum dependencies" + - nosetests --exe -v skimage + + - tools/header.py "Pep8 and Flake tests" + - flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples + + - travis_retry sh tools/travis_get_optional.sh + + - tools/header.py "Run doc examples" + - export PYTHONPATH=$(pwd):$PYTHONPATH + - for f in doc/examples/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done + - for f in doc/examples/applications/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done + + - tools/header.py "Run tests with all dependencies" + # run tests again with optional dependencies to get more coverage + # measure coverage on py3.3 + - if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then + nosetests --exe -v --with-doctest --with-cov --cover-package skimage; + else + nosetests --exe -v --with-doctest skimage; + fi + after_success: - coveralls diff --git a/tools/travis_build.sh b/tools/travis_build.sh deleted file mode 100644 index 7ed37c56..00000000 --- a/tools/travis_build.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/sh -./header.py "Run all tests with minimum dependencies" -nosetests --exe -v skimage - -./header.py "Pep8 and Flake tests" -flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples - -./header.py "Install optional dependencies" - -# Install Qt and then update the Matplotlib settings -if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then - sudo apt-get install -q python-qt4 - SCI_QT_API=PyQt4 - -else - sudo apt-get install -q libqt4-dev - travis_retry pip install PySide $WHEELHOUSE - python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install - SCI_QT_API=PySide -fi - -# Matplotlib settings - must be after we install Pyside -MPL_DIR=$HOME/.config/matplotlib -mkdir -p $MPL_DIR -touch $MPL_DIR/matplotlibrc -echo 'backend : Agg' > $MPL_DIR/matplotlibrc -echo 'backend.qt4 : '$SCI_QT_API >> $MPL_DIR/matplotlibrc - -# imread does NOT support py3.2 -if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then - sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools - travis_retry pip install imread -fi - -# TODO: update when SimpleITK become available on py34 or hopefully pip -if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then - travis_retry easy_install SimpleITK -fi - -travis_retry sudo apt-get install libfreeimage3 -travis_retry pip install astropy - -if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then - travis_retry pip install pyamg -fi - -./header.py "Run doc examples" -PYTHONPATH=$(pwd):$PYTHONPATH -for f in doc/examples/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done -for f in doc/examples/applications/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done - -./header.py "Run tests with all dependencies" -# run tests again with optional dependencies to get more coverage -# measure coverage on py3.3 -if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then - nosetests --exe -v --with-doctest --with-cov --cover-package skimage -else - nosetests --exe -v --with-doctest skimage -fi - diff --git a/tools/travis_get_optional.sh b/tools/travis_get_optional.sh new file mode 100644 index 00000000..b43a44dc --- /dev/null +++ b/tools/travis_get_optional.sh @@ -0,0 +1,39 @@ +#!/usr/bin/sh +./header.py "Install optional dependencies" + +# Install Qt and then update the Matplotlib settings +if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then + sudo apt-get install -q python-qt4 + SCI_QT_API=PyQt4 + +else + sudo apt-get install -q libqt4-dev + pip install PySide $WHEELHOUSE + python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install + SCI_QT_API=PySide +fi + +# Matplotlib settings - must be after we install Pyside +MPL_DIR=$HOME/.config/matplotlib +mkdir -p $MPL_DIR +touch $MPL_DIR/matplotlibrc +echo 'backend : Agg' > $MPL_DIR/matplotlibrc +echo 'backend.qt4 : '$SCI_QT_API >> $MPL_DIR/matplotlibrc + +# imread does NOT support py3.2 +if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then + sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools + pip install imread +fi + +# TODO: update when SimpleITK become available on py34 or hopefully pip +if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then + easy_install SimpleITK +fi + +sudo apt-get install libfreeimage3 +pip install astropy + +if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then + pip install pyamg +fi diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh index ad740a69..af14be4d 100644 --- a/tools/travis_setup.sh +++ b/tools/travis_setup.sh @@ -1,13 +1,13 @@ #!/usr/bin/sh -travis_retry pip install wheel flake8 coveralls nose +pip install wheel flake8 coveralls nose # on Python 2.7, use the system versions of numpy, scipy, and matplotlib # and the minimum version of cython and networkx if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then - travis_retry sudo apt-get install python-scipy python-matplotlib + sudo apt-get install python-scipy python-matplotlib pip install https://github.com/cython/cython/archive/0.19.2.tar.gz pip install https://github.com/networkx/networkx/archive/networkx-1.8.tar.gz fi -travis_retry pip install -r requirements.txt $WHEELHOUSE +pip install -r requirements.txt $WHEELHOUSE python check_bento_build.py From 7cb084b9c4916ff7428efec79bad9acc8625df50 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 20:12:22 -0500 Subject: [PATCH 0535/1122] Use an explicit matrix --- .travis.yml | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4a4288fd..c6bf6f56 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,21 +7,24 @@ language: python -python: - - 2.6 - - "2.7_with_system_site_packages" - - 3.2 - - 3.3 - - 3.4 +env: TEST_ARGS="" -env: - WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" +matrix: + include: + - python: "2.6" + - python: "2.7_with_system_site_packages" + - python: "3.2" + - python: "3.3" + env: TEST_ARGS="--with-cov --cover-package" + - python: "3.4" before_install: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start - sudo apt-get update + - export WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" + - travis_retry sh tools/travis_setup.sh install: @@ -46,13 +49,7 @@ script: - tools/header.py "Run tests with all dependencies" # run tests again with optional dependencies to get more coverage - # measure coverage on py3.3 - - if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then - nosetests --exe -v --with-doctest --with-cov --cover-package skimage; - else - nosetests --exe -v --with-doctest skimage; - fi - + - nosetests --exe -v --with-doctest $TEST_ARGS after_success: - coveralls From d415486a211eb470366a41edc57bf0442ef6b9ac Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 20:26:21 -0500 Subject: [PATCH 0536/1122] Try removing the explicit env --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c6bf6f56..58206877 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,8 +7,6 @@ language: python -env: TEST_ARGS="" - matrix: include: - python: "2.6" From 81d54983382bed9107d805f200186c45aaa09b6e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 20:34:25 -0500 Subject: [PATCH 0537/1122] Put the TEST_ARGS in travis_setup.sh --- .travis.yml | 15 +++++++-------- tools/travis_setup.sh | 6 ++++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 58206877..47983033 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,14 +7,12 @@ language: python -matrix: - include: - - python: "2.6" - - python: "2.7_with_system_site_packages" - - python: "3.2" - - python: "3.3" - env: TEST_ARGS="--with-cov --cover-package" - - python: "3.4" +python: +- 2.6 +- "2.7_with_system_site_packages" +- 3.2 +- 3.3 +- 3.4 before_install: - export DISPLAY=:99.0 @@ -47,6 +45,7 @@ script: - tools/header.py "Run tests with all dependencies" # run tests again with optional dependencies to get more coverage + # we get TEST_ARGS from travis_setup.sh - nosetests --exe -v --with-doctest $TEST_ARGS after_success: diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh index af14be4d..5595f769 100644 --- a/tools/travis_setup.sh +++ b/tools/travis_setup.sh @@ -11,3 +11,9 @@ fi pip install -r requirements.txt $WHEELHOUSE python check_bento_build.py + +if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then + export TEST_ARGS="--with-cov --cover-package skimage" +else + export TEST_ARGS="" +fi From 184b9fece3aad4726faef7d7b406d5b611311d7b Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 21:06:01 -0500 Subject: [PATCH 0538/1122] Specify Qt API --- tools/travis_get_optional.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/travis_get_optional.sh b/tools/travis_get_optional.sh index b43a44dc..45007915 100644 --- a/tools/travis_get_optional.sh +++ b/tools/travis_get_optional.sh @@ -5,12 +5,14 @@ if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then sudo apt-get install -q python-qt4 SCI_QT_API=PyQt4 + export QT_API=pyqt else sudo apt-get install -q libqt4-dev pip install PySide $WHEELHOUSE python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install SCI_QT_API=PySide + export QT_API=PySide fi # Matplotlib settings - must be after we install Pyside From adfe6b3c3e9d223c8d2ac5a046e68388ef5f077d Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 21:17:04 -0500 Subject: [PATCH 0539/1122] Rename variable to MPL_QT_API --- tools/travis_get_optional.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/travis_get_optional.sh b/tools/travis_get_optional.sh index 45007915..ccc297c9 100644 --- a/tools/travis_get_optional.sh +++ b/tools/travis_get_optional.sh @@ -4,14 +4,14 @@ # Install Qt and then update the Matplotlib settings if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then sudo apt-get install -q python-qt4 - SCI_QT_API=PyQt4 + MPL_QT_API=PyQt4 export QT_API=pyqt else sudo apt-get install -q libqt4-dev pip install PySide $WHEELHOUSE python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install - SCI_QT_API=PySide + MPL_QT_API=PySide export QT_API=PySide fi @@ -20,7 +20,7 @@ MPL_DIR=$HOME/.config/matplotlib mkdir -p $MPL_DIR touch $MPL_DIR/matplotlibrc echo 'backend : Agg' > $MPL_DIR/matplotlibrc -echo 'backend.qt4 : '$SCI_QT_API >> $MPL_DIR/matplotlibrc +echo 'backend.qt4 : '$MPL_QT_API >> $MPL_DIR/matplotlibrc # imread does NOT support py3.2 if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then From 5f719d19ebe22be504d858faf4ac83c13a9a4043 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 22:04:42 -0500 Subject: [PATCH 0540/1122] Put the whole script in an sh file --- .travis.yml | 20 ++----------------- tools/travis_get_optional.sh | 38 ++++++++++++++++++++++++++++++++++-- tools/travis_setup.sh | 10 +++------- 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/.travis.yml b/.travis.yml index 47983033..a2fe2e84 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ before_install: - export WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" - - travis_retry sh tools/travis_setup.sh + - ./tools/travis_setup.sh install: - tools/header.py "Dependency versions" @@ -30,23 +30,7 @@ install: - python setup.py build_ext --inplace script: - - tools/header.py "Run all tests with minimum dependencies" - - nosetests --exe -v skimage - - - tools/header.py "Pep8 and Flake tests" - - flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples - - - travis_retry sh tools/travis_get_optional.sh - - - tools/header.py "Run doc examples" - - export PYTHONPATH=$(pwd):$PYTHONPATH - - for f in doc/examples/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done - - for f in doc/examples/applications/*.py; do python "$f"; if [ $? -ne 0 ]; then exit 1; fi done - - - tools/header.py "Run tests with all dependencies" - # run tests again with optional dependencies to get more coverage - # we get TEST_ARGS from travis_setup.sh - - nosetests --exe -v --with-doctest $TEST_ARGS + - ./tools/travis_test.sh after_success: - coveralls diff --git a/tools/travis_get_optional.sh b/tools/travis_get_optional.sh index ccc297c9..1a3a9a59 100644 --- a/tools/travis_get_optional.sh +++ b/tools/travis_get_optional.sh @@ -1,5 +1,13 @@ -#!/usr/bin/sh -./header.py "Install optional dependencies" +#!/bin/sh +set -ex + +tools/header.py "Run all tests with minimum dependencies" +nosetests --exe -v skimage + +tools/header.py "Pep8 and Flake tests" +flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples + +tools/header.py "Install optional dependencies" # Install Qt and then update the Matplotlib settings if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then @@ -39,3 +47,29 @@ pip install astropy if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then pip install pyamg fi + +tools/header.py "Run doc examples" +PYTHONPATH=$(pwd):$PYTHONPATH +for f in doc/examples/*.py; +do python "$f"; + if [ $? -ne 0 ]; then + exit 1; + fi +done + +for f in doc/examples/applications/*.py; +do python "$f"; + if [ $? -ne 0 ]; then + exit 1; + fi +done + +tools/header.py "Run tests with all dependencies" +# run tests again with optional dependencies to get more coverage +if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then + export TEST_ARGS="--with-cov --cover-package skimage" +else + export TEST_ARGS="" +fi +nosetests --exe -v --with-doctest $TEST_ARGS + diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh index 5595f769..9ef667e7 100644 --- a/tools/travis_setup.sh +++ b/tools/travis_setup.sh @@ -1,4 +1,6 @@ -#!/usr/bin/sh +#!/bin/sh +set -ex + pip install wheel flake8 coveralls nose # on Python 2.7, use the system versions of numpy, scipy, and matplotlib @@ -11,9 +13,3 @@ fi pip install -r requirements.txt $WHEELHOUSE python check_bento_build.py - -if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then - export TEST_ARGS="--with-cov --cover-package skimage" -else - export TEST_ARGS="" -fi From b68643afc262b4ed463805029ea6b570237f88bb Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 23:00:26 -0500 Subject: [PATCH 0541/1122] Make shell scripts executable --- tools/travis_get_optional.sh | 0 tools/travis_setup.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 tools/travis_get_optional.sh mode change 100644 => 100755 tools/travis_setup.sh diff --git a/tools/travis_get_optional.sh b/tools/travis_get_optional.sh old mode 100644 new mode 100755 diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh old mode 100644 new mode 100755 From f39775add6794f04628887ff4ee5a1c967ef3104 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 9 Oct 2014 23:01:04 -0500 Subject: [PATCH 0542/1122] Remove the travis numpy --- tools/travis_setup.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh index 9ef667e7..6ab65da3 100755 --- a/tools/travis_setup.sh +++ b/tools/travis_setup.sh @@ -2,6 +2,7 @@ set -ex pip install wheel flake8 coveralls nose +pip uninstall -y numpy # on Python 2.7, use the system versions of numpy, scipy, and matplotlib # and the minimum version of cython and networkx From b594f0afa07f924ea7ebfb37d28849faf94e3154 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 05:00:17 -0500 Subject: [PATCH 0543/1122] Rename the script --- tools/{travis_get_optional.sh => travis_test.sh} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tools/{travis_get_optional.sh => travis_test.sh} (100%) diff --git a/tools/travis_get_optional.sh b/tools/travis_test.sh similarity index 100% rename from tools/travis_get_optional.sh rename to tools/travis_test.sh From ba1193b15ba218c5c01e7738b6b4a8218e810b0c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 05:17:39 -0500 Subject: [PATCH 0544/1122] Switch shell to bash --- tools/travis_setup.sh | 2 +- tools/travis_test.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh index 6ab65da3..4678dafa 100755 --- a/tools/travis_setup.sh +++ b/tools/travis_setup.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/bash set -ex pip install wheel flake8 coveralls nose diff --git a/tools/travis_test.sh b/tools/travis_test.sh index 1a3a9a59..a309a32d 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/bash set -ex tools/header.py "Run all tests with minimum dependencies" From 5511639938578b5ffb8e4517755b8d9a6ea040a5 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 05:25:48 -0500 Subject: [PATCH 0545/1122] Use more generic shebang --- tools/travis_setup.sh | 2 +- tools/travis_test.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh index 4678dafa..c71e3d7a 100755 --- a/tools/travis_setup.sh +++ b/tools/travis_setup.sh @@ -1,4 +1,4 @@ -#!/usr/bin/bash +#!/usr/bin/env bash set -ex pip install wheel flake8 coveralls nose diff --git a/tools/travis_test.sh b/tools/travis_test.sh index a309a32d..c0cab37a 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -1,4 +1,4 @@ -#!/usr/bin/bash +#!#!/usr/bin/env bash set -ex tools/header.py "Run all tests with minimum dependencies" From 10cc476f7f4516ba67d385d1796e65db56e3307d Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 05:33:35 -0500 Subject: [PATCH 0546/1122] Try to get system matplotlib to pass requirments.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 435803b9..98f3b77f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ cython>=0.19.2 -matplotlib>=1.1.1 +matplotlib>=1.1.0 numpy>=1.6.1 scipy>=0.9 six>=1.3 From 29e51a0604e3da65afdadbed161a720ae7aafd68 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 05:36:54 -0500 Subject: [PATCH 0547/1122] Fix shebang line --- tools/travis_test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/travis_test.sh b/tools/travis_test.sh index c0cab37a..acbc1241 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -1,4 +1,4 @@ -#!#!/usr/bin/env bash +#!/usr/bin/env bash set -ex tools/header.py "Run all tests with minimum dependencies" From fdfb24269422948c2b2fdc80379e6375a293b45f Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 05:50:02 -0500 Subject: [PATCH 0548/1122] Update the notes --- doc/travis_notes.txt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/doc/travis_notes.txt b/doc/travis_notes.txt index c5d5c99c..37f5d4a1 100644 --- a/doc/travis_notes.txt +++ b/doc/travis_notes.txt @@ -3,10 +3,19 @@ http://lint.travis-ci.org/ is recommended elsewhere but does not give helpful error reports. - Make sure all of your "-" lines start on the same column -- Use shell scripts for `before_install` and `script` or any part that +- Use bash scripts for `before_install` and `script` or any part that has conditional statements + - Make sure they are "executable" (chmod +x) + - Use the following header: + + ``` + #!/usr/bin/env bash + set -ex + ``` + - Use `travis_retry` before a command to have it try several times before -failing (useful for installing from third party sources) +failing (useful for installing from third party sources). Note that this +command is not available within a bash script. - Feel free to cancel a build rather than waiting for it to go to completion if you have made a change to that branch. - A VM with 64bit Ubuntu 12.04 is a huge help for debugging. From 6e326add184840234f64b7a472449e5da78136d2 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 05:51:03 -0500 Subject: [PATCH 0549/1122] The python path should include the parent directory now --- tools/travis_test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/travis_test.sh b/tools/travis_test.sh index acbc1241..f79cbc2b 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -49,7 +49,7 @@ if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then fi tools/header.py "Run doc examples" -PYTHONPATH=$(pwd):$PYTHONPATH +PYTHONPATH="..":$PYTHONPATH for f in doc/examples/*.py; do python "$f"; if [ $? -ne 0 ]; then From 73e639e3b614ad2b784d507ac708246dde02a974 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 05:53:04 -0500 Subject: [PATCH 0550/1122] Fix formatting of 'for' loops --- tools/travis_test.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/travis_test.sh b/tools/travis_test.sh index f79cbc2b..5f062542 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -50,15 +50,15 @@ fi tools/header.py "Run doc examples" PYTHONPATH="..":$PYTHONPATH -for f in doc/examples/*.py; -do python "$f"; +for f in doc/examples/*.py; do + python "$f"; if [ $? -ne 0 ]; then exit 1; fi done -for f in doc/examples/applications/*.py; -do python "$f"; +for f in doc/examples/applications/*.py; do + python "$f"; if [ $? -ne 0 ]; then exit 1; fi From e146c3eea5ea9449a1601a7d36eba20e9c60b86e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 06:11:26 -0500 Subject: [PATCH 0551/1122] Clean up formatting and a comment --- tools/travis_setup.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh index c71e3d7a..67b0a046 100755 --- a/tools/travis_setup.sh +++ b/tools/travis_setup.sh @@ -6,11 +6,11 @@ pip uninstall -y numpy # on Python 2.7, use the system versions of numpy, scipy, and matplotlib # and the minimum version of cython and networkx -if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then +if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then sudo apt-get install python-scipy python-matplotlib pip install https://github.com/cython/cython/archive/0.19.2.tar.gz pip install https://github.com/networkx/networkx/archive/networkx-1.8.tar.gz fi -pip install -r requirements.txt $WHEELHOUSE +pip install -r requirements.txt $WHEELHOUSE python check_bento_build.py From d3172b33501bb6cbf8b8afad6c831991ac866d72 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 06:12:38 -0500 Subject: [PATCH 0552/1122] Keep consistent with calling convention --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a2fe2e84..028796fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ before_install: - export WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" - - ./tools/travis_setup.sh + - tools/travis_setup.sh install: - tools/header.py "Dependency versions" @@ -30,7 +30,7 @@ install: - python setup.py build_ext --inplace script: - - ./tools/travis_test.sh + - tools/travis_test.sh after_success: - coveralls From 5dbf433eef0aed66f087076637e4f399e686c0f5 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 06:13:26 -0500 Subject: [PATCH 0553/1122] Put the WHEELHOUSE as a global variable --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 028796fa..75f259a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,9 @@ language: python +env: + WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" + python: - 2.6 - "2.7_with_system_site_packages" @@ -19,8 +22,6 @@ before_install: - sh -e /etc/init.d/xvfb start - sudo apt-get update - - export WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" - - tools/travis_setup.sh install: From 16f2393958a86603e62fa8a91b0748a473a46aed Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 06:14:27 -0500 Subject: [PATCH 0554/1122] Put the other env variables up top as well --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 75f259a0..6c67cc17 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,8 @@ language: python env: WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" + PYTHONWARNINGS="all" + DISPLAY=:99.0 python: - 2.6 @@ -18,7 +20,6 @@ python: - 3.4 before_install: - - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start - sudo apt-get update @@ -27,7 +28,6 @@ before_install: install: - tools/header.py "Dependency versions" - tools/build_versions.py - - export PYTHONWARNINGS=all - python setup.py build_ext --inplace script: From 2d4604a3cf71edb394293d04ed591bc2db81f0c5 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 06:16:49 -0500 Subject: [PATCH 0555/1122] Move dependency output to setup script --- .travis.yml | 3 --- tools/travis_setup.sh | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6c67cc17..f1ad920e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,12 +22,9 @@ python: before_install: - sh -e /etc/init.d/xvfb start - sudo apt-get update - - tools/travis_setup.sh install: - - tools/header.py "Dependency versions" - - tools/build_versions.py - python setup.py build_ext --inplace script: diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh index 67b0a046..a542a0ec 100755 --- a/tools/travis_setup.sh +++ b/tools/travis_setup.sh @@ -14,3 +14,6 @@ fi pip install -r requirements.txt $WHEELHOUSE python check_bento_build.py + +tools/header.py "Dependency versions" +tools/build_versions.py From ef215de3a93e1bddf9ff9e56f46d753f51158bbf Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 06:29:42 -0500 Subject: [PATCH 0556/1122] Install skimage so we do not have to fool with PYTHONPATH --- .travis.yml | 1 + tools/travis_test.sh | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f1ad920e..c774db01 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,6 +26,7 @@ before_install: install: - python setup.py build_ext --inplace + - python setup.py install script: - tools/travis_test.sh diff --git a/tools/travis_test.sh b/tools/travis_test.sh index 5f062542..98275418 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -49,7 +49,6 @@ if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then fi tools/header.py "Run doc examples" -PYTHONPATH="..":$PYTHONPATH for f in doc/examples/*.py; do python "$f"; if [ $? -ne 0 ]; then From 9c7cee33ab94215fa6aee0333909990236b545a6 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 06:30:57 -0500 Subject: [PATCH 0557/1122] Use a straight install --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c774db01..9a63850b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,6 @@ before_install: - tools/travis_setup.sh install: - - python setup.py build_ext --inplace - python setup.py install script: From c52a42975d6e69006473e67b6739c0b6962c6fd0 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 06:33:52 -0500 Subject: [PATCH 0558/1122] Move env variable to before_install --- .travis.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9a63850b..5659d255 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,11 +7,6 @@ language: python -env: - WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" - PYTHONWARNINGS="all" - DISPLAY=:99.0 - python: - 2.6 - "2.7_with_system_site_packages" @@ -21,7 +16,10 @@ python: before_install: - sh -e /etc/init.d/xvfb start + - export DISPLAY=:99.0 - sudo apt-get update + - export WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" + - export PYTHONWARNINGS="all" - tools/travis_setup.sh install: From 3a0f0ccb86fd9358303d25c3a9585dd4ddd210ba Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 06:34:35 -0500 Subject: [PATCH 0559/1122] Add whitespace --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 5659d255..ae1d9425 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,7 @@ before_install: - sh -e /etc/init.d/xvfb start - export DISPLAY=:99.0 - sudo apt-get update + - export WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" - export PYTHONWARNINGS="all" - tools/travis_setup.sh From 0e3c8382f34a25eb9b6fb1b44122ade35b502c89 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 06:37:35 -0500 Subject: [PATCH 0560/1122] Remove whitespace --- tools/travis_test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/travis_test.sh b/tools/travis_test.sh index 98275418..71e3ef03 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -31,13 +31,13 @@ echo 'backend : Agg' > $MPL_DIR/matplotlibrc echo 'backend.qt4 : '$MPL_QT_API >> $MPL_DIR/matplotlibrc # imread does NOT support py3.2 -if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then +if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools pip install imread fi # TODO: update when SimpleITK become available on py34 or hopefully pip -if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then +if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then easy_install SimpleITK fi From 095160f1c63327d25cd0f5c98c56f79b869f66cf Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 07:12:38 -0500 Subject: [PATCH 0561/1122] Reinstate the separate build step --- .travis.yml | 1 + tools/travis_test.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ae1d9425..ac551dc5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,7 @@ before_install: - tools/travis_setup.sh install: + - python setup.py build_ext --inplace - python setup.py install script: diff --git a/tools/travis_test.sh b/tools/travis_test.sh index 71e3ef03..8af29582 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -20,7 +20,7 @@ else pip install PySide $WHEELHOUSE python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install MPL_QT_API=PySide - export QT_API=PySide + export QT_API=pyside fi # Matplotlib settings - must be after we install Pyside From 53892e215366d39228a793a621289c242e5c167e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 09:01:18 -0500 Subject: [PATCH 0562/1122] Move the rest of the code to the scripts. --- .travis.yml | 6 ------ tools/travis_setup.sh | 5 +++++ tools/travis_test.sh | 4 ++++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index ac551dc5..2b6451ca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,12 +15,6 @@ python: - 3.4 before_install: - - sh -e /etc/init.d/xvfb start - - export DISPLAY=:99.0 - - sudo apt-get update - - - export WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" - - export PYTHONWARNINGS="all" - tools/travis_setup.sh install: diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh index a542a0ec..f7602aef 100755 --- a/tools/travis_setup.sh +++ b/tools/travis_setup.sh @@ -1,6 +1,11 @@ #!/usr/bin/env bash set -ex +sh -e /etc/init.d/xvfb start +sudo apt-get update + +WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" + pip install wheel flake8 coveralls nose pip uninstall -y numpy diff --git a/tools/travis_test.sh b/tools/travis_test.sh index 8af29582..62cfe948 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash set -ex +export DISPLAY=:99.0 +export PYTHONWARNINGS="all" +WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" + tools/header.py "Run all tests with minimum dependencies" nosetests --exe -v skimage From b37913d63c7853539c1f19c130df78598203a0df Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 11:28:33 -0500 Subject: [PATCH 0563/1122] Use a null backend during the doc examples --- tools/travis_test.sh | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tools/travis_test.sh b/tools/travis_test.sh index 62cfe948..25245ee2 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -27,13 +27,6 @@ else export QT_API=pyside fi -# Matplotlib settings - must be after we install Pyside -MPL_DIR=$HOME/.config/matplotlib -mkdir -p $MPL_DIR -touch $MPL_DIR/matplotlibrc -echo 'backend : Agg' > $MPL_DIR/matplotlibrc -echo 'backend.qt4 : '$MPL_QT_API >> $MPL_DIR/matplotlibrc - # imread does NOT support py3.2 if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools @@ -52,6 +45,13 @@ if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then pip install pyamg fi +# Matplotlib settings - do not show figures during doc examples +MPL_DIR=$HOME/.config/matplotlib +mkdir -p $MPL_DIR +touch $MPL_DIR/matplotlibrc +echo 'backend : Template' > $MPL_DIR/matplotlibrc + + tools/header.py "Run doc examples" for f in doc/examples/*.py; do python "$f"; @@ -67,6 +67,11 @@ for f in doc/examples/applications/*.py; do fi done +# Now configure Matplotlib to use Qt4 +echo 'backend: Agg' > $MPL_DIR/matplotlibrc +echo 'backend.qt4 : '$MPL_QT_API >> $MPL_DIR/matplotlibrc + + tools/header.py "Run tests with all dependencies" # run tests again with optional dependencies to get more coverage if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then From f5b30bf86750115922eaa9744053ce0b4cff0488 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 20:15:33 -0500 Subject: [PATCH 0564/1122] Formatting --- tools/travis_test.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/travis_test.sh b/tools/travis_test.sh index 25245ee2..6ed95049 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -54,16 +54,16 @@ echo 'backend : Template' > $MPL_DIR/matplotlibrc tools/header.py "Run doc examples" for f in doc/examples/*.py; do - python "$f"; + python "$f" if [ $? -ne 0 ]; then - exit 1; + exit 1 fi done for f in doc/examples/applications/*.py; do - python "$f"; + python "$f" if [ $? -ne 0 ]; then - exit 1; + exit 1 fi done From 283e759b378ec4cf8eca388e1fbfc93b380b4501 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 10 Oct 2014 22:07:04 -0500 Subject: [PATCH 0565/1122] Matplotlibrc was in a different location in mpl 1.1.1 --- tools/travis_test.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/travis_test.sh b/tools/travis_test.sh index 6ed95049..c3e14423 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -17,6 +17,7 @@ tools/header.py "Install optional dependencies" if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then sudo apt-get install -q python-qt4 MPL_QT_API=PyQt4 + MPL_DIR=$HOME/.matplotlibrc export QT_API=pyqt else @@ -24,6 +25,7 @@ else pip install PySide $WHEELHOUSE python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install MPL_QT_API=PySide + MPL_DIR=$HOME/.config/matplotlibrc export QT_API=pyside fi @@ -46,7 +48,6 @@ if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then fi # Matplotlib settings - do not show figures during doc examples -MPL_DIR=$HOME/.config/matplotlib mkdir -p $MPL_DIR touch $MPL_DIR/matplotlibrc echo 'backend : Template' > $MPL_DIR/matplotlibrc From 3b63e19412f9aa1e4b20cd0285bb810561cd356e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 11 Oct 2014 04:37:00 -0500 Subject: [PATCH 0566/1122] Fix MPL_DIR location --- tools/travis_test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/travis_test.sh b/tools/travis_test.sh index c3e14423..df3a484d 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -17,7 +17,7 @@ tools/header.py "Install optional dependencies" if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then sudo apt-get install -q python-qt4 MPL_QT_API=PyQt4 - MPL_DIR=$HOME/.matplotlibrc + MPL_DIR=$HOME/.matplotlib export QT_API=pyqt else @@ -25,7 +25,7 @@ else pip install PySide $WHEELHOUSE python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install MPL_QT_API=PySide - MPL_DIR=$HOME/.config/matplotlibrc + MPL_DIR=$HOME/.config/matplotlib export QT_API=pyside fi From 0c35d132032d23cc9b4b2a18119eb9fda1179aa9 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 11 Oct 2014 05:12:04 -0500 Subject: [PATCH 0567/1122] Add a try/catch for RectangleTool.disconnect_events --- skimage/viewer/canvastools/recttool.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/canvastools/recttool.py b/skimage/viewer/canvastools/recttool.py index 5d658533..25b6df88 100644 --- a/skimage/viewer/canvastools/recttool.py +++ b/skimage/viewer/canvastools/recttool.py @@ -50,7 +50,10 @@ class RectangleTool(CanvasToolBase, RectangleSelector): props['edgecolor'] = props['facecolor'] RectangleSelector.__init__(self, self.ax, lambda *args: None, rectprops=props) - self.disconnect_events() # events are handled by the viewer + try: + self.disconnect_events() # events are handled by the viewer + except AttributeError: + pass # older versions of MPL do not have this method # Alias rectangle attribute, which is initialized in RectangleSelector. self._rect = self.to_draw self._rect.set_animated(True) From 532e46017240701d481ae85a4285778ec232bb39 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 11 Oct 2014 07:21:32 -0500 Subject: [PATCH 0568/1122] Disconnect the events manually --- skimage/viewer/canvastools/recttool.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/viewer/canvastools/recttool.py b/skimage/viewer/canvastools/recttool.py index 25b6df88..fcd10aca 100644 --- a/skimage/viewer/canvastools/recttool.py +++ b/skimage/viewer/canvastools/recttool.py @@ -50,10 +50,10 @@ class RectangleTool(CanvasToolBase, RectangleSelector): props['edgecolor'] = props['facecolor'] RectangleSelector.__init__(self, self.ax, lambda *args: None, rectprops=props) - try: - self.disconnect_events() # events are handled by the viewer - except AttributeError: - pass # older versions of MPL do not have this method + # Events are handled by the viewer + for c in self.cids: + self.canvas.mpl_disconnect(c) + # Alias rectangle attribute, which is initialized in RectangleSelector. self._rect = self.to_draw self._rect.set_animated(True) From 72292f73c0a77a4489b9889f86b8e32d4270cb30 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 11 Oct 2014 09:15:10 -0500 Subject: [PATCH 0569/1122] Use a manual hack to remove the canvas events for old MPL --- skimage/viewer/canvastools/recttool.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/skimage/viewer/canvastools/recttool.py b/skimage/viewer/canvastools/recttool.py index fcd10aca..ce81c38d 100644 --- a/skimage/viewer/canvastools/recttool.py +++ b/skimage/viewer/canvastools/recttool.py @@ -51,8 +51,11 @@ class RectangleTool(CanvasToolBase, RectangleSelector): RectangleSelector.__init__(self, self.ax, lambda *args: None, rectprops=props) # Events are handled by the viewer - for c in self.cids: - self.canvas.mpl_disconnect(c) + try: + self.disconnect_events() + except AttributeError: + # disconnect the events manually (hack for older mpl verions) + [self.canvas.mpl_disconnect(i) for i in range(5)] # Alias rectangle attribute, which is initialized in RectangleSelector. self._rect = self.to_draw From 725c376d78d7d8e6a7c49e1352ada7864f1c5c92 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 11 Oct 2014 14:34:55 -0500 Subject: [PATCH 0570/1122] Make sure we disconnect all the signals --- skimage/viewer/canvastools/recttool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/viewer/canvastools/recttool.py b/skimage/viewer/canvastools/recttool.py index ce81c38d..41e9ab1e 100644 --- a/skimage/viewer/canvastools/recttool.py +++ b/skimage/viewer/canvastools/recttool.py @@ -55,7 +55,7 @@ class RectangleTool(CanvasToolBase, RectangleSelector): self.disconnect_events() except AttributeError: # disconnect the events manually (hack for older mpl verions) - [self.canvas.mpl_disconnect(i) for i in range(5)] + [self.canvas.mpl_disconnect(i) for i in range(10)] # Alias rectangle attribute, which is initialized in RectangleSelector. self._rect = self.to_draw From 0fabb0c3ab45fd805a6d623979b0cb8b96e75d02 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 11 Oct 2014 18:28:36 -0500 Subject: [PATCH 0571/1122] Fix comment typo --- skimage/viewer/canvastools/recttool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/viewer/canvastools/recttool.py b/skimage/viewer/canvastools/recttool.py index 41e9ab1e..8932e180 100644 --- a/skimage/viewer/canvastools/recttool.py +++ b/skimage/viewer/canvastools/recttool.py @@ -54,7 +54,7 @@ class RectangleTool(CanvasToolBase, RectangleSelector): try: self.disconnect_events() except AttributeError: - # disconnect the events manually (hack for older mpl verions) + # disconnect the events manually (hack for older mpl versions) [self.canvas.mpl_disconnect(i) for i in range(10)] # Alias rectangle attribute, which is initialized in RectangleSelector. From 36ab379e0c9fb80786d77488a2f81cb5bd224c84 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 06:08:35 -0500 Subject: [PATCH 0572/1122] Use the PyPI tifffile instead of bundling --- DEPENDS.txt | 1 + bento.info | 3 - requirements.txt | 1 + skimage/io/_plugins/tifffile.c | 962 ------ skimage/io/_plugins/tifffile.py | 4847 ------------------------------- skimage/io/setup.py | 4 - 6 files changed, 2 insertions(+), 5816 deletions(-) delete mode 100644 skimage/io/_plugins/tifffile.c delete mode 100644 skimage/io/_plugins/tifffile.py diff --git a/DEPENDS.txt b/DEPENDS.txt index aa3cd021..e5c10521 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -16,6 +16,7 @@ Runtime requirements * `NetworkX `__ * `Pillow `__ (or `PIL `__) +* `Tifffile `__ Known build errors ------------------ diff --git a/bento.info b/bento.info index bcec295d..564f1f5a 100644 --- a/bento.info +++ b/bento.info @@ -157,9 +157,6 @@ Library: Extension: skimage.graph._ncut_cy Sources: skimage/graph/_ncut_cy.pyx - Extension: skimage.io._plugins._tifffile - Sources: - skimage/io/_plugins/tifffile.c Executable: skivi Module: skimage.scripts.skivi diff --git a/requirements.txt b/requirements.txt index 98f3b77f..85f5a1f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ scipy>=0.9 six>=1.3 networkx>=1.8 pillow>=1.1.7 +tifffile>=0.1 diff --git a/skimage/io/_plugins/tifffile.c b/skimage/io/_plugins/tifffile.c deleted file mode 100644 index 6aa0eaa1..00000000 --- a/skimage/io/_plugins/tifffile.c +++ /dev/null @@ -1,962 +0,0 @@ - - -/* tifffile.c - -A Python C extension module for decoding PackBits and LZW encoded TIFF data. - -Refer to the tifffile.py module for documentation and tests. - -:Author: - `Christoph Gohlke `_ - -:Organization: - Laboratory for Fluorescence Dynamics, University of California, Irvine - -:Version: 2013.11.05 - -Install -------- -Use this Python distutils setup script to build the extension module:: - - # setup.py - # Usage: ``python setup.py build_ext --inplace`` - from distutils.core import setup, Extension - import numpy - setup(name='_tifffile', - ext_modules=[Extension('_tifffile', ['tifffile.c'], - include_dirs=[numpy.get_include()])]) - -License -------- -Copyright (c) 2008-2014, Christoph Gohlke -Copyright (c) 2008-2014, The Regents of the University of California -Produced at the Laboratory for Fluorescence Dynamics -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -* Neither the name of the copyright holders nor the names of any - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. -*/ - -#define _VERSION_ "2013.11.05" - -#define WIN32_LEAN_AND_MEAN -#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION - -#include "Python.h" -#include "string.h" -#include "numpy/arrayobject.h" - -/* little endian by default */ -#ifndef MSB -#define MSB 1 -#endif - -#if MSB -#define LSB 0 -#define BOC '<' -#else -#define LSB 1 -#define BOC '>' -#endif - -#define NO_ERROR 0 -#define VALUE_ERROR -1 - -#ifdef _MSC_VER -typedef unsigned __int8 uint8_t; -typedef unsigned __int16 uint16_t; -typedef unsigned __int32 uint32_t; -typedef unsigned __int64 uint64_t; -#ifdef _WIN64 -typedef __int64 ssize_t; -typedef signed __int64 intptr_t; -typedef unsigned __int64 uintptr_t; -#define SSIZE_MAX (9223372036854775808) -#else -typedef int ssize_t; -typedef _W64 signed int intptr_t; -typedef _W64 unsigned int uintptr_t; -#define SSIZE_MAX (2147483648) -#endif -#else -/* non MS compilers */ -#include -#include -#endif - -#define SWAP2BYTES(x) \ - ((((x) >> 8) & 0x00FF) | (((x) & 0x00FF) << 8)) - -#define SWAP4BYTES(x) \ - ((((x) >> 24) & 0x00FF) | (((x)&0x00FF) << 24) | \ - (((x) >> 8 ) & 0xFF00) | (((x)&0xFF00) << 8)) - -#define SWAP8BYTES(x) \ - ((((x) >> 56) & 0x00000000000000FF) | (((x) >> 40) & 0x000000000000FF00) | \ - (((x) >> 24) & 0x0000000000FF0000) | (((x) >> 8) & 0x00000000FF000000) | \ - (((x) << 8) & 0x000000FF00000000) | (((x) << 24) & 0x0000FF0000000000) | \ - (((x) << 40) & 0x00FF000000000000) | (((x) << 56) & 0xFF00000000000000)) - -struct BYTE_STRING { - unsigned int ref; /* reference count */ - unsigned int len; /* length of string */ - char *str; /* pointer to bytes */ -}; - -typedef union { - uint8_t b[2]; - uint16_t i; -} u_uint16; - -typedef union { - uint8_t b[4]; - uint32_t i; -} u_uint32; - -typedef union { - uint8_t b[8]; - uint64_t i; -} u_uint64; - -/*****************************************************************************/ -/* C functions */ - -/* Return mask for itemsize bits */ -unsigned char bitmask(const int itemsize) { - unsigned char result = 0; - unsigned char power = 1; - int i; - for (i = 0; i < itemsize; i++) { - result += power; - power *= 2; - } - return result << (8 - itemsize); -} - -/** Unpack sequence of tigthly packed 1-32 bit integers. - -Native byte order will be returned. - -Input data array should be padded to the next 16, 32 or 64-bit boundary -if itemsize not in (1, 2, 4, 8, 16, 24, 32, 64). - -*/ -int unpackbits( - unsigned char *data, - const ssize_t size, /** size of data in bytes */ - const int itemsize, /** number of bits in integer */ - ssize_t numitems, /** number of items to unpack */ - unsigned char *result /** buffer to store unpacked items */ - ) -{ - ssize_t i, j, k, storagesize; - unsigned char value; - /* Input validation is done in wrapper function */ - storagesize = (ssize_t)(ceil(itemsize / 8.0)); - storagesize = storagesize < 3 ? storagesize : storagesize > 4 ? 8 : 4; - switch (itemsize) { - case 8: - case 16: - case 32: - case 64: - memcpy(result, data, numitems*storagesize); - return NO_ERROR; - case 1: - for (i = 0, j = 0; i < numitems/8; i++) { - value = data[i]; - result[j++] = (value & (unsigned char)(128)) >> 7; - result[j++] = (value & (unsigned char)(64)) >> 6; - result[j++] = (value & (unsigned char)(32)) >> 5; - result[j++] = (value & (unsigned char)(16)) >> 4; - result[j++] = (value & (unsigned char)(8)) >> 3; - result[j++] = (value & (unsigned char)(4)) >> 2; - result[j++] = (value & (unsigned char)(2)) >> 1; - result[j++] = (value & (unsigned char)(1)); - } - if (numitems % 8) { - value = data[i]; - switch (numitems % 8) { - case 7: result[j+6] = (value & (unsigned char)(2)) >> 1; - case 6: result[j+5] = (value & (unsigned char)(4)) >> 2; - case 5: result[j+4] = (value & (unsigned char)(8)) >> 3; - case 4: result[j+3] = (value & (unsigned char)(16)) >> 4; - case 3: result[j+2] = (value & (unsigned char)(32)) >> 5; - case 2: result[j+1] = (value & (unsigned char)(64)) >> 6; - case 1: result[j] = (value & (unsigned char)(128)) >> 7; - } - } - return NO_ERROR; - case 2: - for (i = 0, j = 0; i < numitems/4; i++) { - value = data[i]; - result[j++] = (value & (unsigned char)(192)) >> 6; - result[j++] = (value & (unsigned char)(48)) >> 4; - result[j++] = (value & (unsigned char)(12)) >> 2; - result[j++] = (value & (unsigned char)(3)); - } - if (numitems % 4) { - value = data[i]; - switch (numitems % 4) { - case 3: result[j+2] = (value & (unsigned char)(12)) >> 2; - case 2: result[j+1] = (value & (unsigned char)(48)) >> 4; - case 1: result[j] = (value & (unsigned char)(192)) >> 6; - } - } - return NO_ERROR; - case 4: - for (i = 0, j = 0; i < numitems/2; i++) { - value = data[i]; - result[j++] = (value & (unsigned char)(240)) >> 4; - result[j++] = (value & (unsigned char)(15)); - } - if (numitems % 2) { - value = data[i]; - result[j] = (value & (unsigned char)(240)) >> 4; - } - return NO_ERROR; - case 24: - j = k = 0; - for (i = 0; i < numitems; i++) { - result[j++] = 0; - result[j++] = data[k++]; - result[j++] = data[k++]; - result[j++] = data[k++]; - } - return NO_ERROR; - } - /* 3, 5, 6, 7 */ - if (itemsize < 8) { - int shr = 16; - u_uint16 value, mask, tmp; - j = k = 0; - value.b[MSB] = data[j++]; - value.b[LSB] = data[j++]; - mask.b[MSB] = bitmask(itemsize); - mask.b[LSB] = 0; - for (i = 0; i < numitems; i++) { - shr -= itemsize; - tmp.i = (value.i & mask.i) >> shr; - result[k++] = tmp.b[LSB]; - if (shr < itemsize) { - value.b[MSB] = value.b[LSB]; - value.b[LSB] = data[j++]; - mask.i <<= 8 - itemsize; - shr += 8; - } else { - mask.i >>= itemsize; - } - } - return NO_ERROR; - } - /* 9, 10, 11, 12, 13, 14, 15 */ - if (itemsize < 16) { - int shr = 32; - u_uint32 value, mask, tmp; - mask.i = 0; - j = k = 0; -#if MSB - for (i = 3; i >= 0; i--) { - value.b[i] = data[j++]; - } - mask.b[3] = 0xFF; - mask.b[2] = bitmask(itemsize-8); - for (i = 0; i < numitems; i++) { - shr -= itemsize; - tmp.i = (value.i & mask.i) >> shr; - result[k++] = tmp.b[0]; /* swap bytes */ - result[k++] = tmp.b[1]; - if (shr < itemsize) { - value.b[3] = value.b[1]; - value.b[2] = value.b[0]; - value.b[1] = data[j++]; - value.b[0] = data[j++]; - mask.i <<= 16 - itemsize; - shr += 16; - } else { - mask.i >>= itemsize; - } - } -#else - /* not implemented */ -#endif - return NO_ERROR; - } - /* 17, 18, 19, 20, 21, 22, 23, 25, 26, 27, 28, 29, 30, 31 */ - if (itemsize < 32) { - int shr = 64; - u_uint64 value, mask, tmp; - mask.i = 0; - j = k = 0; -#if MSB - for (i = 7; i >= 0; i--) { - value.b[i] = data[j++]; - } - mask.b[7] = 0xFF; - mask.b[6] = 0xFF; - mask.b[5] = itemsize > 23 ? 0xFF : bitmask(itemsize-16); - mask.b[4] = itemsize < 24 ? 0x00 : bitmask(itemsize-24); - for (i = 0; i < numitems; i++) { - shr -= itemsize; - tmp.i = (value.i & mask.i) >> shr; - result[k++] = tmp.b[0]; /* swap bytes */ - result[k++] = tmp.b[1]; - result[k++] = tmp.b[2]; - result[k++] = tmp.b[3]; - if (shr < itemsize) { - value.b[7] = value.b[3]; - value.b[6] = value.b[2]; - value.b[5] = value.b[1]; - value.b[4] = value.b[0]; - value.b[3] = data[j++]; - value.b[2] = data[j++]; - value.b[1] = data[j++]; - value.b[0] = data[j++]; - mask.i <<= 32 - itemsize; - shr += 32; - } else { - mask.i >>= itemsize; - } - } -#else - /* Not implemented */ -#endif - return NO_ERROR; - } - return VALUE_ERROR; -} - -/*****************************************************************************/ -/* Python functions */ - -/** Unpack tightly packed integers. */ -char py_unpackints_doc[] = "Unpack groups of bits into numpy array."; - -static PyObject* -py_unpackints(PyObject *obj, PyObject *args, PyObject *kwds) -{ - PyObject *byteobj = NULL; - PyArrayObject *result = NULL; - PyArray_Descr *dtype = NULL; - char *encoded = NULL; - char *decoded = NULL; - Py_ssize_t encoded_len = 0; - Py_ssize_t decoded_len = 0; - Py_ssize_t runlen = 0; - Py_ssize_t i; - int storagesize, bytesize; - int itemsize = 0; - int skipbits = 0; - static char *kwlist[] = {"data", "dtype", "itemsize", "runlen", NULL}; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO&i|i", kwlist, - &byteobj, PyArray_DescrConverter, &dtype, &itemsize, &runlen)) - return NULL; - - Py_INCREF(byteobj); - - if (((itemsize < 1) || (itemsize > 32)) && (itemsize != 64)) { - PyErr_Format(PyExc_ValueError, "itemsize out of range"); - goto _fail; - } - - if (!PyBytes_Check(byteobj)) { - PyErr_Format(PyExc_TypeError, "expected byte string as input"); - goto _fail; - } - - encoded = PyBytes_AS_STRING(byteobj); - encoded_len = PyBytes_GET_SIZE(byteobj); - bytesize = (int)ceil(itemsize / 8.0); - storagesize = bytesize < 3 ? bytesize : bytesize > 4 ? 8 : 4; - if ((encoded_len < bytesize) || (encoded_len > SSIZE_MAX / storagesize)) { - PyErr_Format(PyExc_ValueError, "data size out of range"); - goto _fail; - } - if (dtype->elsize != storagesize) { - PyErr_Format(PyExc_TypeError, "dtype.elsize doesn't fit itemsize"); - goto _fail; - } - - if (runlen == 0) { - runlen = (Py_ssize_t)(((uint64_t)encoded_len*8) / (uint64_t)itemsize); - } - skipbits = (Py_ssize_t)(((uint64_t)runlen * (uint64_t)itemsize) % 8); - if (skipbits > 0) { - skipbits = 8 - skipbits; - } - decoded_len = (Py_ssize_t)((uint64_t)runlen * (((uint64_t)encoded_len*8) / - ((uint64_t)runlen*(uint64_t)itemsize + (uint64_t)skipbits))); - - result = (PyArrayObject *)PyArray_SimpleNew(1, &decoded_len, - dtype->type_num); - if (result == NULL) { - PyErr_Format(PyExc_MemoryError, "unable to allocate output array"); - goto _fail; - } - decoded = (char *)PyArray_DATA(result); - - for (i = 0; i < decoded_len; i+=runlen) { - if (NO_ERROR != - unpackbits((unsigned char *) encoded, - (ssize_t) encoded_len, - (int) itemsize, - (ssize_t) runlen, - (unsigned char *) decoded)) { - PyErr_Format(PyExc_ValueError, "unpackbits() failed"); - goto _fail; - } - encoded += (Py_ssize_t)(((uint64_t)runlen * (uint64_t)itemsize + - (uint64_t)skipbits) / 8); - decoded += runlen * storagesize; - } - - if ((dtype->byteorder != BOC) && (itemsize % 8 == 0)) { - switch (dtype->elsize) { - case 2: { - uint16_t *d = (uint16_t *)PyArray_DATA(result); - for (i = 0; i < PyArray_SIZE(result); i++) { - *d = SWAP2BYTES(*d); d++; - } - break; } - case 4: { - uint32_t *d = (uint32_t *)PyArray_DATA(result); - for (i = 0; i < PyArray_SIZE(result); i++) { - *d = SWAP4BYTES(*d); d++; - } - break; } - case 8: { - uint64_t *d = (uint64_t *)PyArray_DATA(result); - for (i = 0; i < PyArray_SIZE(result); i++) { - *d = SWAP8BYTES(*d); d++; - } - break; } - } - } - Py_DECREF(byteobj); - Py_DECREF(dtype); - return PyArray_Return(result); - - _fail: - Py_XDECREF(byteobj); - Py_XDECREF(result); - Py_XDECREF(dtype); - return NULL; -} - - -/** Decode TIFF PackBits encoded string. */ -char py_decodepackbits_doc[] = "Return TIFF PackBits decoded string."; - -static PyObject * -py_decodepackbits(PyObject *obj, PyObject *args) -{ - int n; - char e; - char *decoded = NULL; - char *encoded = NULL; - char *encoded_end = NULL; - char *encoded_pos = NULL; - unsigned int encoded_len; - unsigned int decoded_len; - PyObject *byteobj = NULL; - PyObject *result = NULL; - - if (!PyArg_ParseTuple(args, "O", &byteobj)) - return NULL; - - if (!PyBytes_Check(byteobj)) { - PyErr_Format(PyExc_TypeError, "expected byte string as input"); - goto _fail; - } - - Py_INCREF(byteobj); - encoded = PyBytes_AS_STRING(byteobj); - encoded_len = (unsigned int)PyBytes_GET_SIZE(byteobj); - - /* release GIL: byte/string objects are immutable */ - Py_BEGIN_ALLOW_THREADS - - /* determine size of decoded string */ - encoded_pos = encoded; - encoded_end = encoded + encoded_len; - decoded_len = 0; - while (encoded_pos < encoded_end) { - n = (int)*encoded_pos++; - if (n >= 0) { - n++; - if (encoded_pos+n > encoded_end) - n = (int)(encoded_end - encoded_pos); - encoded_pos += n; - decoded_len += n; - } else if (n > -128) { - encoded_pos++; - decoded_len += 1-n; - } - } - Py_END_ALLOW_THREADS - - result = PyBytes_FromStringAndSize(0, decoded_len); - if (result == NULL) { - PyErr_Format(PyExc_MemoryError, "failed to allocate decoded string"); - goto _fail; - } - decoded = PyBytes_AS_STRING(result); - - Py_BEGIN_ALLOW_THREADS - - /* decode string */ - encoded_end = encoded + encoded_len; - while (encoded < encoded_end) { - n = (int)*encoded++; - if (n >= 0) { - n++; - if (encoded+n > encoded_end) - n = (int)(encoded_end - encoded); - /* memmove(decoded, encoded, n); decoded += n; encoded += n; */ - while (n--) - *decoded++ = *encoded++; - } else if (n > -128) { - n = 1 - n; - e = *encoded++; - /* memset(decoded, e, n); decoded += n; */ - while (n--) - *decoded++ = e; - } - } - Py_END_ALLOW_THREADS - - Py_DECREF(byteobj); - return result; - - _fail: - Py_XDECREF(byteobj); - Py_XDECREF(result); - return NULL; -} - - -/** Decode TIFF LZW encoded string. */ -char py_decodelzw_doc[] = "Return TIFF LZW decoded string."; - -static PyObject * -py_decodelzw(PyObject *obj, PyObject *args) -{ - PyThreadState *_save = NULL; - PyObject *byteobj = NULL; - PyObject *result = NULL; - int i, j; - unsigned int encoded_len = 0; - unsigned int decoded_len = 0; - unsigned int result_len = 0; - unsigned int table_len = 0; - unsigned int len; - unsigned int code, c, oldcode, mask, shr; - uint64_t bitcount, bitw; - char *encoded = NULL; - char *result_ptr = NULL; - char *table2 = NULL; - char *cptr; - struct BYTE_STRING *decoded = NULL; - struct BYTE_STRING *decoded_ptr = NULL; - struct BYTE_STRING *table[4096]; - struct BYTE_STRING *newentry, *newresult, *t; - int little_endian = 0; - - if (!PyArg_ParseTuple(args, "O", &byteobj)) - return NULL; - - if (!PyBytes_Check(byteobj)) { - PyErr_Format(PyExc_TypeError, "expected byte string as input"); - goto _fail; - } - - Py_INCREF(byteobj); - encoded = PyBytes_AS_STRING(byteobj); - encoded_len = (unsigned int)PyBytes_GET_SIZE(byteobj); - /* - if (encoded_len >= 512 * 1024 * 1024) { - PyErr_Format(PyExc_ValueError, "encoded data > 512 MB not supported"); - goto _fail; - } - */ - /* release GIL: byte/string objects are immutable */ - _save = PyEval_SaveThread(); - - if ((*encoded != -128) || ((*(encoded+1) & 128))) { - PyEval_RestoreThread(_save); - PyErr_Format(PyExc_ValueError, - "strip must begin with CLEAR code"); - goto _fail; - } - little_endian = (*(unsigned short *)encoded) & 128; - - /* allocate buffer for codes and pointers */ - decoded_len = 0; - len = (encoded_len + encoded_len/9) * sizeof(decoded); - decoded = PyMem_Malloc(len * sizeof(void *)); - if (decoded == NULL) { - PyEval_RestoreThread(_save); - PyErr_Format(PyExc_MemoryError, "failed to allocate decoded"); - goto _fail; - } - memset((void *)decoded, 0, len * sizeof(void *)); - decoded_ptr = decoded; - - /* cache strings of length 2 */ - cptr = table2 = PyMem_Malloc(256*256*2 * sizeof(char)); - if (table2 == NULL) { - PyEval_RestoreThread(_save); - PyErr_Format(PyExc_MemoryError, "failed to allocate table2"); - goto _fail; - } - for (i = 0; i < 256; i++) { - for (j = 0; j < 256; j++) { - *cptr++ = (char)i; - *cptr++ = (char)j; - } - } - - memset(table, 0, sizeof(table)); - table_len = 258; - bitw = 9; - shr = 23; - mask = 4286578688; - bitcount = 0; - result_len = 0; - code = 0; - oldcode = 0; - - while ((unsigned int)((bitcount + bitw) / 8) <= encoded_len) { - /* read next code */ - code = *((unsigned int *)((void *)(encoded + (bitcount / 8)))); - if (little_endian) - code = SWAP4BYTES(code); - code <<= (unsigned int)(bitcount % 8); - code &= mask; - code >>= shr; - bitcount += bitw; - - if (code == 257) /* end of information */ - break; - - if (code == 256) { /* clearcode */ - /* initialize table and switch to 9 bit */ - while (table_len > 258) { - t = table[--table_len]; - t->ref--; - if (t->ref == 0) { - if (t->len > 2) - PyMem_Free(t->str); - PyMem_Free(t); - } - } - bitw = 9; - shr = 23; - mask = 4286578688; - - /* read next code */ - code = *((unsigned int *)((void *)(encoded + (bitcount / 8)))); - if (little_endian) - code = SWAP4BYTES(code); - code <<= bitcount % 8; - code &= mask; - code >>= shr; - bitcount += bitw; - - if (code == 257) /* end of information */ - break; - - /* decoded.append(table[code]) */ - if (code < 256) { - result_len++; - *((int *)decoded_ptr++) = code; - } else { - newresult = table[code]; - newresult->ref++; - result_len += newresult->len; - *(struct BYTE_STRING **)decoded_ptr++ = newresult; - } - } else { - if (code < table_len) { - /* code is in table */ - /* newresult = table[code]; */ - /* newentry = table[oldcode] + table[code][0] */ - /* decoded.append(newresult); table.append(newentry) */ - if (code < 256) { - c = code; - *((unsigned int *)decoded_ptr++) = code; - result_len++; - } else { - newresult = table[code]; - newresult->ref++; - c = (unsigned int) *newresult->str; - *(struct BYTE_STRING **)decoded_ptr++ = newresult; - result_len += newresult->len; - } - newentry = PyMem_Malloc(sizeof(struct BYTE_STRING)); - newentry->ref = 1; - if (oldcode < 256) { - newentry->len = 2; - newentry->str = table2 + (oldcode << 9) + - ((unsigned char)c << 1); - } else { - len = table[oldcode]->len; - newentry->len = len + 1; - newentry->str = PyMem_Malloc(newentry->len); - if (newentry->str == NULL) - break; - memmove(newentry->str, table[oldcode]->str, len); - newentry->str[len] = c; - } - table[table_len++] = newentry; - } else { - /* code is not in table */ - /* newentry = newresult = table[oldcode] + table[oldcode][0] */ - /* decoded.append(newresult); table.append(newentry) */ - newresult = PyMem_Malloc(sizeof(struct BYTE_STRING)); - newentry = newresult; - newentry->ref = 2; - if (oldcode < 256) { - newentry->len = 2; - newentry->str = table2 + 514*oldcode; - } else { - len = table[oldcode]->len; - newentry->len = len + 1; - newentry->str = PyMem_Malloc(newentry->len); - if (newentry->str == NULL) - break; - memmove(newentry->str, table[oldcode]->str, len); - newentry->str[len] = *table[oldcode]->str; - } - table[table_len++] = newentry; - *(struct BYTE_STRING **)decoded_ptr++ = newresult; - result_len += newresult->len; - } - } - oldcode = code; - /* increase bit-width if necessary */ - switch (table_len) { - case 511: - bitw = 10; - shr = 22; - mask = 4290772992; - break; - case 1023: - bitw = 11; - shr = 21; - mask = 4292870144; - break; - case 2047: - bitw = 12; - shr = 20; - mask = 4293918720; - } - } - - PyEval_RestoreThread(_save); - - if (code != 257) { - PyErr_WarnEx(NULL, - "py_decodelzw encountered unexpected end of stream", 1); - } - - /* result = ''.join(decoded) */ - decoded_len = (unsigned int)(decoded_ptr - decoded); - decoded_ptr = decoded; - result = PyBytes_FromStringAndSize(0, result_len); - if (result == NULL) { - PyErr_Format(PyExc_MemoryError, "failed to allocate decoded string"); - goto _fail; - } - result_ptr = PyBytes_AS_STRING(result); - - _save = PyEval_SaveThread(); - - while (decoded_len--) { - code = *((unsigned int *)decoded_ptr); - if (code < 256) { - *result_ptr++ = (char)code; - } else { - t = *((struct BYTE_STRING **)decoded_ptr); - memmove(result_ptr, t->str, t->len); - result_ptr += t->len; - if (--t->ref == 0) { - if (t->len > 2) - PyMem_Free(t->str); - PyMem_Free(t); - } - } - decoded_ptr++; - } - PyMem_Free(decoded); - - while (table_len-- > 258) { - t = table[table_len]; - if (t->len > 2) - PyMem_Free(t->str); - PyMem_Free(t); - } - PyMem_Free(table2); - - PyEval_RestoreThread(_save); - - Py_DECREF(byteobj); - return result; - - _fail: - if (table2 != NULL) - PyMem_Free(table2); - if (decoded != NULL) { - /* Bug? are decoded_ptr and decoded_len correct? */ - while (decoded_len--) { - code = *((unsigned int *) decoded_ptr); - if (code > 258) { - t = *((struct BYTE_STRING **) decoded_ptr); - if (--t->ref == 0) { - if (t->len > 2) - PyMem_Free(t->str); - PyMem_Free(t); - } - } - } - PyMem_Free(decoded); - } - while (table_len-- > 258) { - t = table[table_len]; - if (t->len > 2) - PyMem_Free(t->str); - PyMem_Free(t); - } - - Py_XDECREF(byteobj); - Py_XDECREF(result); - - return NULL; -} - -/*****************************************************************************/ -/* Create Python module */ - -char module_doc[] = - "A Python C extension module for decoding PackBits and LZW encoded " - "TIFF data.\n\n" - "Refer to the tifffile.py module for documentation and tests.\n\n" - "Authors:\n Christoph Gohlke \n" - " Laboratory for Fluorescence Dynamics, University of California, Irvine." - "\n\nVersion: %s\n"; - -static PyMethodDef module_methods[] = { -#if MSB - {"unpackints", (PyCFunction)py_unpackints, METH_VARARGS|METH_KEYWORDS, - py_unpackints_doc}, -#endif - {"decodelzw", (PyCFunction)py_decodelzw, METH_VARARGS, - py_decodelzw_doc}, - {"decodepackbits", (PyCFunction)py_decodepackbits, METH_VARARGS, - py_decodepackbits_doc}, - {NULL, NULL, 0, NULL} /* Sentinel */ -}; - -#if PY_MAJOR_VERSION >= 3 - -struct module_state { - PyObject *error; -}; - -#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) - -static int module_traverse(PyObject *m, visitproc visit, void *arg) { - Py_VISIT(GETSTATE(m)->error); - return 0; -} - -static int module_clear(PyObject *m) { - Py_CLEAR(GETSTATE(m)->error); - return 0; -} - -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_tifffile", - NULL, - sizeof(struct module_state), - module_methods, - NULL, - module_traverse, - module_clear, - NULL -}; - -#define INITERROR return NULL - -PyMODINIT_FUNC -PyInit__tifffile(void) - -#else - -#define INITERROR return - -PyMODINIT_FUNC -init_tifffile(void) - -#endif -{ - PyObject *module; - - char *doc = (char *)PyMem_Malloc(sizeof(module_doc) + sizeof(_VERSION_)); - PyOS_snprintf(doc, sizeof(doc), module_doc, _VERSION_); - -#if PY_MAJOR_VERSION >= 3 - moduledef.m_doc = doc; - module = PyModule_Create(&moduledef); -#else - module = Py_InitModule3("_tifffile", module_methods, doc); -#endif - - PyMem_Free(doc); - - if (module == NULL) - INITERROR; - - if (_import_array() < 0) { - Py_DECREF(module); - INITERROR; - } - - { -#if PY_MAJOR_VERSION < 3 - PyObject *s = PyString_FromString(_VERSION_); -#else - PyObject *s = PyUnicode_FromString(_VERSION_); -#endif - PyObject *dict = PyModule_GetDict(module); - PyDict_SetItemString(dict, "__version__", s); - Py_DECREF(s); - } - -#if PY_MAJOR_VERSION >= 3 - return module; -#endif -} - diff --git a/skimage/io/_plugins/tifffile.py b/skimage/io/_plugins/tifffile.py deleted file mode 100644 index 11744289..00000000 --- a/skimage/io/_plugins/tifffile.py +++ /dev/null @@ -1,4847 +0,0 @@ - - -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# tifffile.py - -# Copyright (c) 2008-2014, Christoph Gohlke -# Copyright (c) 2008-2014, The Regents of the University of California -# Produced at the Laboratory for Fluorescence Dynamics -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of the copyright holders nor the names of any -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -"""Read and write image data from and to TIFF files. - -Image and metadata can be read from TIFF, BigTIFF, OME-TIFF, STK, LSM, NIH, -SGI, ImageJ, MicroManager, FluoView, SEQ and GEL files. -Only a subset of the TIFF specification is supported, mainly uncompressed -and losslessly compressed 2**(0 to 6) bit integer, 16, 32 and 64-bit float, -grayscale and RGB(A) images, which are commonly used in bio-scientific imaging. -Specifically, reading JPEG and CCITT compressed image data or EXIF, IPTC, GPS, -and XMP metadata is not implemented. -Only primary info records are read for STK, FluoView, MicroManager, and -NIH image formats. - -TIFF, the Tagged Image File Format, is under the control of Adobe Systems. -BigTIFF allows for files greater than 4 GB. STK, LSM, FluoView, SGI, SEQ, GEL, -and OME-TIFF, are custom extensions defined by Molecular Devices (Universal -Imaging Corporation), Carl Zeiss MicroImaging, Olympus, Silicon Graphics -International, Media Cybernetics, Molecular Dynamics, and the Open Microscopy -Environment consortium respectively. - -For command line usage run ``python tifffile.py --help`` - -:Author: - `Christoph Gohlke `_ - -:Organization: - Laboratory for Fluorescence Dynamics, University of California, Irvine - -:Version: 2014.08.24 - -Requirements ------------- -* `CPython 2.7 or 3.4 `_ -* `Numpy 1.8.2 `_ -* `Matplotlib 1.4 `_ (optional for plotting) -* `Tifffile.c 2013.11.05 `_ - (recommended for faster decoding of PackBits and LZW encoded strings) - -Notes ------ -The API is not stable yet and might change between revisions. - -Tested on little-endian platforms only. - -Other Python packages and modules for reading bio-scientific TIFF files: - -* `Imread `_ -* `PyLibTiff `_ -* `SimpleITK `_ -* `PyLSM `_ -* `PyMca.TiffIO.py `_ (same as fabio.TiffIO) -* `BioImageXD.Readers `_ -* `Cellcognition.io `_ -* `CellProfiler.bioformats - `_ - -Acknowledgements ----------------- -* Egor Zindy, University of Manchester, for cz_lsm_scan_info specifics. -* Wim Lewis for a bug fix and some read_cz_lsm functions. -* Hadrien Mary for help on reading MicroManager files. - -References ----------- -(1) TIFF 6.0 Specification and Supplements. Adobe Systems Incorporated. - http://partners.adobe.com/public/developer/tiff/ -(2) TIFF File Format FAQ. http://www.awaresystems.be/imaging/tiff/faq.html -(3) MetaMorph Stack (STK) Image File Format. - http://support.meta.moleculardevices.com/docs/t10243.pdf -(4) Image File Format Description LSM 5/7 Release 6.0 (ZEN 2010). - Carl Zeiss MicroImaging GmbH. BioSciences. May 10, 2011 -(5) File Format Description - LSM 5xx Release 2.0. - http://ibb.gsf.de/homepage/karsten.rodenacker/IDL/Lsmfile.doc -(6) The OME-TIFF format. - http://www.openmicroscopy.org/site/support/file-formats/ome-tiff -(7) UltraQuant(r) Version 6.0 for Windows Start-Up Guide. - http://www.ultralum.com/images%20ultralum/pdf/UQStart%20Up%20Guide.pdf -(8) Micro-Manager File Formats. - http://www.micro-manager.org/wiki/Micro-Manager_File_Formats -(9) Tags for TIFF and Related Specifications. Digital Preservation. - http://www.digitalpreservation.gov/formats/content/tiff_tags.shtml - -Examples --------- ->>> data = numpy.random.rand(5, 301, 219) ->>> imsave('temp.tif', data) - ->>> image = imread('temp.tif') ->>> numpy.testing.assert_array_equal(image, data) - ->>> with TiffFile('temp.tif') as tif: -... images = tif.asarray() -... for page in tif: -... for tag in page.tags.values(): -... t = tag.name, tag.value -... image = page.asarray() - -""" - -from __future__ import division, print_function - -import sys -import os -import re -import glob -import math -import zlib -import time -import json -import struct -import warnings -import tempfile -import datetime -import collections -from fractions import Fraction -from xml.etree import cElementTree as etree - -import numpy - -try: - from . import _tifffile -except ImportError: - warnings.warn( - "failed to import the optional _tifffile C extension module.\n" - "Loading of some compressed images will be slow.\n" - "Tifffile.c can be obtained at http://www.lfd.uci.edu/~gohlke/") - -__version__ = '2014.08.24' -__docformat__ = 'restructuredtext en' -__all__ = ('imsave', 'imread', 'imshow', 'TiffFile', 'TiffWriter', - 'TiffSequence') - - -def imsave(filename, data, **kwargs): - """Write image data to TIFF file. - - Refer to the TiffWriter class and member functions for documentation. - - Parameters - ---------- - filename : str - Name of file to write. - data : array_like - Input image. The last dimensions are assumed to be image depth, - height, width, and samples. - kwargs : dict - Parameters 'byteorder', 'bigtiff', and 'software' are passed to - the TiffWriter class. - Parameters 'photometric', 'planarconfig', 'resolution', - 'description', 'compress', 'volume', and 'extratags' are passed to - the TiffWriter.save function. - - Examples - -------- - >>> data = numpy.random.rand(2, 5, 3, 301, 219) - >>> description = u'{"shape": %s}' % str(list(data.shape)) - >>> imsave('temp.tif', data, compress=6, - ... extratags=[(270, 's', 0, description, True)]) - - """ - tifargs = {} - for key in ('byteorder', 'bigtiff', 'software', 'writeshape'): - if key in kwargs: - tifargs[key] = kwargs[key] - del kwargs[key] - - if 'writeshape' not in kwargs: - kwargs['writeshape'] = True - if 'bigtiff' not in tifargs and data.size*data.dtype.itemsize > 2000*2**20: - tifargs['bigtiff'] = True - - with TiffWriter(filename, **tifargs) as tif: - tif.save(data, **kwargs) - - -class TiffWriter(object): - """Write image data to TIFF file. - - TiffWriter instances must be closed using the close method, which is - automatically called when using the 'with' statement. - - Examples - -------- - >>> data = numpy.random.rand(2, 5, 3, 301, 219) - >>> with TiffWriter('temp.tif', bigtiff=True) as tif: - ... for i in range(data.shape[0]): - ... tif.save(data[i], compress=6) - - """ - TYPES = {'B': 1, 's': 2, 'H': 3, 'I': 4, '2I': 5, 'b': 6, - 'h': 8, 'i': 9, 'f': 11, 'd': 12, 'Q': 16, 'q': 17} - TAGS = { - 'new_subfile_type': 254, 'subfile_type': 255, - 'image_width': 256, 'image_length': 257, 'bits_per_sample': 258, - 'compression': 259, 'photometric': 262, 'fill_order': 266, - 'document_name': 269, 'image_description': 270, 'strip_offsets': 273, - 'orientation': 274, 'samples_per_pixel': 277, 'rows_per_strip': 278, - 'strip_byte_counts': 279, 'x_resolution': 282, 'y_resolution': 283, - 'planar_configuration': 284, 'page_name': 285, 'resolution_unit': 296, - 'software': 305, 'datetime': 306, 'predictor': 317, 'color_map': 320, - 'tile_width': 322, 'tile_length': 323, 'tile_offsets': 324, - 'tile_byte_counts': 325, 'extra_samples': 338, 'sample_format': 339, - 'image_depth': 32997, 'tile_depth': 32998} - - def __init__(self, filename, bigtiff=False, byteorder=None, - software='tifffile.py'): - """Create a new TIFF file for writing. - - Use bigtiff=True when creating files greater than 2 GB. - - Parameters - ---------- - filename : str - Name of file to write. - bigtiff : bool - If True, the BigTIFF format is used. - byteorder : {'<', '>'} - The endianness of the data in the file. - By default this is the system's native byte order. - software : str - Name of the software used to create the image. - Saved with the first page only. - - """ - if byteorder not in (None, '<', '>'): - raise ValueError("invalid byteorder %s" % byteorder) - if byteorder is None: - byteorder = '<' if sys.byteorder == 'little' else '>' - - self._byteorder = byteorder - self._software = software - - self._fh = open(filename, 'wb') - self._fh.write({'<': b'II', '>': b'MM'}[byteorder]) - - if bigtiff: - self._bigtiff = True - self._offset_size = 8 - self._tag_size = 20 - self._numtag_format = 'Q' - self._offset_format = 'Q' - self._val_format = '8s' - self._fh.write(struct.pack(byteorder+'HHH', 43, 8, 0)) - else: - self._bigtiff = False - self._offset_size = 4 - self._tag_size = 12 - self._numtag_format = 'H' - self._offset_format = 'I' - self._val_format = '4s' - self._fh.write(struct.pack(byteorder+'H', 42)) - - # first IFD - self._ifd_offset = self._fh.tell() - self._fh.write(struct.pack(byteorder+self._offset_format, 0)) - - def save(self, data, photometric=None, planarconfig=None, resolution=None, - description=None, volume=False, writeshape=False, compress=0, - extratags=()): - """Write image data to TIFF file. - - Image data are written in one stripe per plane. - Dimensions larger than 2 to 4 (depending on photometric mode, planar - configuration, and SGI mode) are flattened and saved as separate pages. - The 'sample_format' and 'bits_per_sample' TIFF tags are derived from - the data type. - - Parameters - ---------- - data : array_like - Input image. The last dimensions are assumed to be image depth, - height, width, and samples. - photometric : {'minisblack', 'miniswhite', 'rgb'} - The color space of the image data. - By default this setting is inferred from the data shape. - planarconfig : {'contig', 'planar'} - Specifies if samples are stored contiguous or in separate planes. - By default this setting is inferred from the data shape. - 'contig': last dimension contains samples. - 'planar': third last dimension contains samples. - resolution : (float, float) or ((int, int), (int, int)) - X and Y resolution in dots per inch as float or rational numbers. - description : str - The subject of the image. Saved with the first page only. - compress : int - Values from 0 to 9 controlling the level of zlib compression. - If 0, data are written uncompressed (default). - volume : bool - If True, volume data are stored in one tile (if applicable) using - the SGI image_depth and tile_depth tags. - Image width and depth must be multiple of 16. - Few software can read this format, e.g. MeVisLab. - writeshape : bool - If True, write the data shape to the image_description tag - if necessary and no other description is given. - extratags: sequence of tuples - Additional tags as [(code, dtype, count, value, writeonce)]. - - code : int - The TIFF tag Id. - dtype : str - Data type of items in 'value' in Python struct format. - One of B, s, H, I, 2I, b, h, i, f, d, Q, or q. - count : int - Number of data values. Not used for string values. - value : sequence - 'Count' values compatible with 'dtype'. - writeonce : bool - If True, the tag is written to the first page only. - - """ - if photometric not in (None, 'minisblack', 'miniswhite', 'rgb'): - raise ValueError("invalid photometric %s" % photometric) - if planarconfig not in (None, 'contig', 'planar'): - raise ValueError("invalid planarconfig %s" % planarconfig) - if not 0 <= compress <= 9: - raise ValueError("invalid compression level %s" % compress) - - fh = self._fh - byteorder = self._byteorder - numtag_format = self._numtag_format - val_format = self._val_format - offset_format = self._offset_format - offset_size = self._offset_size - tag_size = self._tag_size - - data = numpy.asarray(data, dtype=byteorder+data.dtype.char, order='C') - data_shape = shape = data.shape - data = numpy.atleast_2d(data) - - # normalize shape of data - samplesperpixel = 1 - extrasamples = 0 - if volume and data.ndim < 3: - volume = False - if photometric is None: - if planarconfig: - photometric = 'rgb' - elif data.ndim > 2 and shape[-1] in (3, 4): - photometric = 'rgb' - elif volume and data.ndim > 3 and shape[-4] in (3, 4): - photometric = 'rgb' - elif data.ndim > 2 and shape[-3] in (3, 4): - photometric = 'rgb' - else: - photometric = 'minisblack' - if planarconfig and len(shape) <= (3 if volume else 2): - planarconfig = None - photometric = 'minisblack' - if photometric == 'rgb': - if len(shape) < 3: - raise ValueError("not a RGB(A) image") - if len(shape) < 4: - volume = False - if planarconfig is None: - if shape[-1] in (3, 4): - planarconfig = 'contig' - elif shape[-4 if volume else -3] in (3, 4): - planarconfig = 'planar' - elif shape[-1] > shape[-4 if volume else -3]: - planarconfig = 'planar' - else: - planarconfig = 'contig' - if planarconfig == 'contig': - data = data.reshape((-1, 1) + shape[(-4 if volume else -3):]) - samplesperpixel = data.shape[-1] - else: - data = data.reshape( - (-1,) + shape[(-4 if volume else -3):] + (1,)) - samplesperpixel = data.shape[1] - if samplesperpixel > 3: - extrasamples = samplesperpixel - 3 - elif planarconfig and len(shape) > (3 if volume else 2): - if planarconfig == 'contig': - data = data.reshape((-1, 1) + shape[(-4 if volume else -3):]) - samplesperpixel = data.shape[-1] - else: - data = data.reshape( - (-1,) + shape[(-4 if volume else -3):] + (1,)) - samplesperpixel = data.shape[1] - extrasamples = samplesperpixel - 1 - else: - planarconfig = None - # remove trailing 1s - while len(shape) > 2 and shape[-1] == 1: - shape = shape[:-1] - if len(shape) < 3: - volume = False - if False and ( - len(shape) > (3 if volume else 2) and shape[-1] < 5 and - all(shape[-1] < i - for i in shape[(-4 if volume else -3):-1])): - # DISABLED: non-standard TIFF, e.g. (220, 320, 2) - planarconfig = 'contig' - samplesperpixel = shape[-1] - data = data.reshape((-1, 1) + shape[(-4 if volume else -3):]) - else: - data = data.reshape( - (-1, 1) + shape[(-3 if volume else -2):] + (1,)) - - if samplesperpixel == 2: - warnings.warn("writing non-standard TIFF (samplesperpixel 2)") - - if volume and (data.shape[-2] % 16 or data.shape[-3] % 16): - warnings.warn("volume width or length are not multiple of 16") - volume = False - data = numpy.swapaxes(data, 1, 2) - data = data.reshape( - (data.shape[0] * data.shape[1],) + data.shape[2:]) - - # data.shape is now normalized 5D or 6D, depending on volume - # (pages, planar_samples, (depth,) height, width, contig_samples) - assert len(data.shape) in (5, 6) - shape = data.shape - - bytestr = bytes if sys.version[0] == '2' else ( - lambda x: bytes(x, 'utf-8') if isinstance(x, str) else x) - tags = [] # list of (code, ifdentry, ifdvalue, writeonce) - - if volume: - # use tiles to save volume data - tag_byte_counts = TiffWriter.TAGS['tile_byte_counts'] - tag_offsets = TiffWriter.TAGS['tile_offsets'] - else: - # else use strips - tag_byte_counts = TiffWriter.TAGS['strip_byte_counts'] - tag_offsets = TiffWriter.TAGS['strip_offsets'] - - def pack(fmt, *val): - return struct.pack(byteorder+fmt, *val) - - def addtag(code, dtype, count, value, writeonce=False): - # Compute ifdentry & ifdvalue bytes from code, dtype, count, value. - # Append (code, ifdentry, ifdvalue, writeonce) to tags list. - code = int(TiffWriter.TAGS.get(code, code)) - try: - tifftype = TiffWriter.TYPES[dtype] - except KeyError: - raise ValueError("unknown dtype %s" % dtype) - rawcount = count - if dtype == 's': - value = bytestr(value) + b'\0' - count = rawcount = len(value) - value = (value, ) - if len(dtype) > 1: - count *= int(dtype[:-1]) - dtype = dtype[-1] - ifdentry = [pack('HH', code, tifftype), - pack(offset_format, rawcount)] - ifdvalue = None - if count == 1: - if isinstance(value, (tuple, list)): - value = value[0] - ifdentry.append(pack(val_format, pack(dtype, value))) - elif struct.calcsize(dtype) * count <= offset_size: - ifdentry.append(pack(val_format, - pack(str(count)+dtype, *value))) - else: - ifdentry.append(pack(offset_format, 0)) - ifdvalue = pack(str(count)+dtype, *value) - tags.append((code, b''.join(ifdentry), ifdvalue, writeonce)) - - def rational(arg, max_denominator=1000000): - # return nominator and denominator from float or two integers - try: - f = Fraction.from_float(arg) - except TypeError: - f = Fraction(arg[0], arg[1]) - f = f.limit_denominator(max_denominator) - return f.numerator, f.denominator - - if self._software: - addtag('software', 's', 0, self._software, writeonce=True) - self._software = None # only save to first page - if description: - addtag('image_description', 's', 0, description, writeonce=True) - elif writeshape and shape[0] > 1 and shape != data_shape: - addtag('image_description', 's', 0, - "shape=(%s)" % (",".join('%i' % i for i in data_shape)), - writeonce=True) - addtag('datetime', 's', 0, - datetime.datetime.now().strftime("%Y:%m:%d %H:%M:%S"), - writeonce=True) - addtag('compression', 'H', 1, 32946 if compress else 1) - addtag('orientation', 'H', 1, 1) - addtag('image_width', 'I', 1, shape[-2]) - addtag('image_length', 'I', 1, shape[-3]) - if volume: - addtag('image_depth', 'I', 1, shape[-4]) - addtag('tile_depth', 'I', 1, shape[-4]) - addtag('tile_width', 'I', 1, shape[-2]) - addtag('tile_length', 'I', 1, shape[-3]) - addtag('new_subfile_type', 'I', 1, 0 if shape[0] == 1 else 2) - addtag('sample_format', 'H', 1, - {'u': 1, 'i': 2, 'f': 3, 'c': 6}[data.dtype.kind]) - addtag('photometric', 'H', 1, - {'miniswhite': 0, 'minisblack': 1, 'rgb': 2}[photometric]) - addtag('samples_per_pixel', 'H', 1, samplesperpixel) - if planarconfig and samplesperpixel > 1: - addtag('planar_configuration', 'H', 1, 1 - if planarconfig == 'contig' else 2) - addtag('bits_per_sample', 'H', samplesperpixel, - (data.dtype.itemsize * 8, ) * samplesperpixel) - else: - addtag('bits_per_sample', 'H', 1, data.dtype.itemsize * 8) - if extrasamples: - if photometric == 'rgb' and extrasamples == 1: - addtag('extra_samples', 'H', 1, 1) # associated alpha channel - else: - addtag('extra_samples', 'H', extrasamples, (0,) * extrasamples) - if resolution: - addtag('x_resolution', '2I', 1, rational(resolution[0])) - addtag('y_resolution', '2I', 1, rational(resolution[1])) - addtag('resolution_unit', 'H', 1, 2) - addtag('rows_per_strip', 'I', 1, - shape[-3] * (shape[-4] if volume else 1)) - - # use one strip or tile per plane - strip_byte_counts = (data[0, 0].size * data.dtype.itemsize,) * shape[1] - addtag(tag_byte_counts, offset_format, shape[1], strip_byte_counts) - addtag(tag_offsets, offset_format, shape[1], (0, ) * shape[1]) - - # add extra tags from users - for t in extratags: - addtag(*t) - # the entries in an IFD must be sorted in ascending order by tag code - tags = sorted(tags, key=lambda x: x[0]) - - if not self._bigtiff and (fh.tell() + data.size*data.dtype.itemsize - > 2**31-1): - raise ValueError("data too large for non-bigtiff file") - - for pageindex in range(shape[0]): - # update pointer at ifd_offset - pos = fh.tell() - fh.seek(self._ifd_offset) - fh.write(pack(offset_format, pos)) - fh.seek(pos) - - # write ifdentries - fh.write(pack(numtag_format, len(tags))) - tag_offset = fh.tell() - fh.write(b''.join(t[1] for t in tags)) - self._ifd_offset = fh.tell() - fh.write(pack(offset_format, 0)) # offset to next IFD - - # write tag values and patch offsets in ifdentries, if necessary - for tagindex, tag in enumerate(tags): - if tag[2]: - pos = fh.tell() - fh.seek(tag_offset + tagindex*tag_size + offset_size + 4) - fh.write(pack(offset_format, pos)) - fh.seek(pos) - if tag[0] == tag_offsets: - strip_offsets_offset = pos - elif tag[0] == tag_byte_counts: - strip_byte_counts_offset = pos - fh.write(tag[2]) - - # write image data - data_offset = fh.tell() - if compress: - strip_byte_counts = [] - for plane in data[pageindex]: - plane = zlib.compress(plane, compress) - strip_byte_counts.append(len(plane)) - fh.write(plane) - else: - # if this fails try update Python/numpy - data[pageindex].tofile(fh) - fh.flush() - - # update strip and tile offsets and byte_counts if necessary - pos = fh.tell() - for tagindex, tag in enumerate(tags): - if tag[0] == tag_offsets: # strip or tile offsets - if tag[2]: - fh.seek(strip_offsets_offset) - strip_offset = data_offset - for size in strip_byte_counts: - fh.write(pack(offset_format, strip_offset)) - strip_offset += size - else: - fh.seek(tag_offset + tagindex*tag_size + - offset_size + 4) - fh.write(pack(offset_format, data_offset)) - elif tag[0] == tag_byte_counts: # strip or tile byte_counts - if compress: - if tag[2]: - fh.seek(strip_byte_counts_offset) - for size in strip_byte_counts: - fh.write(pack(offset_format, size)) - else: - fh.seek(tag_offset + tagindex*tag_size + - offset_size + 4) - fh.write(pack(offset_format, strip_byte_counts[0])) - break - fh.seek(pos) - fh.flush() - # remove tags that should be written only once - if pageindex == 0: - tags = [t for t in tags if not t[-1]] - - def close(self): - self._fh.close() - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - -def imread(files, **kwargs): - """Return image data from TIFF file(s) as numpy array. - - The first image series is returned if no arguments are provided. - - Parameters - ---------- - files : str or list - File name, glob pattern, or list of file names. - key : int, slice, or sequence of page indices - Defines which pages to return as array. - series : int - Defines which series of pages in file to return as array. - multifile : bool - If True (default), OME-TIFF data may include pages from multiple files. - pattern : str - Regular expression pattern that matches axes names and indices in - file names. - kwargs : dict - Additional parameters passed to the TiffFile or TiffSequence asarray - function. - - Examples - -------- - >>> im = imread('test.tif', key=0) - >>> im.shape - (256, 256, 4) - >>> ims = imread(['test.tif', 'test.tif']) - >>> ims.shape - (2, 256, 256, 4) - - """ - kwargs_file = {} - if 'multifile' in kwargs: - kwargs_file['multifile'] = kwargs['multifile'] - del kwargs['multifile'] - else: - kwargs_file['multifile'] = True - kwargs_seq = {} - if 'pattern' in kwargs: - kwargs_seq['pattern'] = kwargs['pattern'] - del kwargs['pattern'] - - if isinstance(files, basestring) and any(i in files for i in '?*'): - files = glob.glob(files) - if not files: - raise ValueError('no files found') - if len(files) == 1: - files = files[0] - - if isinstance(files, basestring): - with TiffFile(files, **kwargs_file) as tif: - return tif.asarray(**kwargs) - else: - with TiffSequence(files, **kwargs_seq) as imseq: - return imseq.asarray(**kwargs) - - -class lazyattr(object): - """Lazy object attribute whose value is computed on first access.""" - __slots__ = ('func', ) - - def __init__(self, func): - self.func = func - - def __get__(self, instance, owner): - if instance is None: - return self - value = self.func(instance) - if value is NotImplemented: - return getattr(super(owner, instance), self.func.__name__) - setattr(instance, self.func.__name__, value) - return value - - -class TiffFile(object): - """Read image and metadata from TIFF, STK, LSM, and FluoView files. - - TiffFile instances must be closed using the close method, which is - automatically called when using the 'with' statement. - - Attributes - ---------- - pages : list - All TIFF pages in file. - series : list of Records(shape, dtype, axes, TiffPages) - TIFF pages with compatible shapes and types. - micromanager_metadata: dict - Extra MicroManager non-TIFF metadata in the file, if exists. - - All attributes are read-only. - - Examples - -------- - >>> with TiffFile('test.tif') as tif: - ... data = tif.asarray() - ... data.shape - (256, 256, 4) - - """ - def __init__(self, arg, name=None, offset=None, size=None, - multifile=True, multifile_close=True): - """Initialize instance from file. - - Parameters - ---------- - arg : str or open file - Name of file or open file object. - The file objects are closed in TiffFile.close(). - name : str - Optional name of file in case 'arg' is a file handle. - offset : int - Optional start position of embedded file. By default this is - the current file position. - size : int - Optional size of embedded file. By default this is the number - of bytes from the 'offset' to the end of the file. - multifile : bool - If True (default), series may include pages from multiple files. - Currently applies to OME-TIFF only. - multifile_close : bool - If True (default), keep the handles of other files in multifile - series closed. This is inefficient when few files refer to - many pages. If False, the C runtime may run out of resources. - - """ - self._fh = FileHandle(arg, name=name, offset=offset, size=size) - self.offset_size = None - self.pages = [] - self._multifile = bool(multifile) - self._multifile_close = bool(multifile_close) - self._files = {self._fh.name: self} # cache of TiffFiles - try: - self._fromfile() - except Exception: - self._fh.close() - raise - - @property - def filehandle(self): - """Return file handle.""" - return self._fh - - @property - def filename(self): - """Return name of file handle.""" - return self._fh.name - - def close(self): - """Close open file handle(s).""" - for tif in self._files.values(): - tif._fh.close() - self._files = {} - - def _fromfile(self): - """Read TIFF header and all page records from file.""" - self._fh.seek(0) - try: - self.byteorder = {b'II': '<', b'MM': '>'}[self._fh.read(2)] - except KeyError: - raise ValueError("not a valid TIFF file") - version = struct.unpack(self.byteorder+'H', self._fh.read(2))[0] - if version == 43: # BigTiff - self.offset_size, zero = struct.unpack(self.byteorder+'HH', - self._fh.read(4)) - if zero or self.offset_size != 8: - raise ValueError("not a valid BigTIFF file") - elif version == 42: - self.offset_size = 4 - else: - raise ValueError("not a TIFF file") - self.pages = [] - while True: - try: - page = TiffPage(self) - self.pages.append(page) - except StopIteration: - break - if not self.pages: - raise ValueError("empty TIFF file") - - if self.is_micromanager: - # MicroManager files contain metadata not stored in TIFF tags. - self.micromanager_metadata = read_micromanager_metadata(self._fh) - - if self.is_lsm: - self._fix_lsm_strip_offsets() - self._fix_lsm_strip_byte_counts() - - def _fix_lsm_strip_offsets(self): - """Unwrap strip offsets for LSM files greater than 4 GB.""" - for series in self.series: - wrap = 0 - previous_offset = 0 - for page in series.pages: - strip_offsets = [] - for current_offset in page.strip_offsets: - if current_offset < previous_offset: - wrap += 2**32 - strip_offsets.append(current_offset + wrap) - previous_offset = current_offset - page.strip_offsets = tuple(strip_offsets) - - def _fix_lsm_strip_byte_counts(self): - """Set strip_byte_counts to size of compressed data. - - The strip_byte_counts tag in LSM files contains the number of bytes - for the uncompressed data. - - """ - if not self.pages: - return - strips = {} - for page in self.pages: - assert len(page.strip_offsets) == len(page.strip_byte_counts) - for offset, bytecount in zip(page.strip_offsets, - page.strip_byte_counts): - strips[offset] = bytecount - offsets = sorted(strips.keys()) - offsets.append(min(offsets[-1] + strips[offsets[-1]], self._fh.size)) - for i, offset in enumerate(offsets[:-1]): - strips[offset] = min(strips[offset], offsets[i+1] - offset) - for page in self.pages: - if page.compression: - page.strip_byte_counts = tuple( - strips[offset] for offset in page.strip_offsets) - - @lazyattr - def series(self): - """Return series of TiffPage with compatible shape and properties.""" - if not self.pages: - return [] - - series = [] - page0 = self.pages[0] - - if self.is_ome: - series = self._omeseries() - elif self.is_fluoview: - dims = {b'X': 'X', b'Y': 'Y', b'Z': 'Z', b'T': 'T', - b'WAVELENGTH': 'C', b'TIME': 'T', b'XY': 'R', - b'EVENT': 'V', b'EXPOSURE': 'L'} - mmhd = list(reversed(page0.mm_header.dimensions)) - series = [Record( - axes=''.join(dims.get(i[0].strip().upper(), 'Q') - for i in mmhd if i[1] > 1), - shape=tuple(int(i[1]) for i in mmhd if i[1] > 1), - pages=self.pages, dtype=numpy.dtype(page0.dtype))] - elif self.is_lsm: - lsmi = page0.cz_lsm_info - axes = CZ_SCAN_TYPES[lsmi.scan_type] - if page0.is_rgb: - axes = axes.replace('C', '').replace('XY', 'XYC') - axes = axes[::-1] - shape = tuple(getattr(lsmi, CZ_DIMENSIONS[i]) for i in axes) - pages = [p for p in self.pages if not p.is_reduced] - series = [Record(axes=axes, shape=shape, pages=pages, - dtype=numpy.dtype(pages[0].dtype))] - if len(pages) != len(self.pages): # reduced RGB pages - pages = [p for p in self.pages if p.is_reduced] - cp = 1 - i = 0 - while cp < len(pages) and i < len(shape)-2: - cp *= shape[i] - i += 1 - shape = shape[:i] + pages[0].shape - axes = axes[:i] + 'CYX' - series.append(Record(axes=axes, shape=shape, pages=pages, - dtype=numpy.dtype(pages[0].dtype))) - elif self.is_imagej: - shape = [] - axes = [] - ij = page0.imagej_tags - if 'frames' in ij: - shape.append(ij['frames']) - axes.append('T') - if 'slices' in ij: - shape.append(ij['slices']) - axes.append('Z') - if 'channels' in ij and not self.is_rgb: - shape.append(ij['channels']) - axes.append('C') - remain = len(self.pages) // (product(shape) if shape else 1) - if remain > 1: - shape.append(remain) - axes.append('I') - shape.extend(page0.shape) - axes.extend(page0.axes) - axes = ''.join(axes) - series = [Record(pages=self.pages, shape=tuple(shape), axes=axes, - dtype=numpy.dtype(page0.dtype))] - elif self.is_nih: - if len(self.pages) == 1: - shape = page0.shape - axes = page0.axes - else: - shape = (len(self.pages),) + page0.shape - axes = 'I' + page0.axes - series = [Record(pages=self.pages, shape=shape, axes=axes, - dtype=numpy.dtype(page0.dtype))] - elif page0.is_shaped: - # TODO: shaped files can contain multiple series - shape = page0.tags['image_description'].value[7:-1] - shape = tuple(int(i) for i in shape.split(b',')) - series = [Record(pages=self.pages, shape=shape, - axes='Q' * len(shape), - dtype=numpy.dtype(page0.dtype))] - - # generic detection of series - if not series: - shapes = [] - pages = {} - for page in self.pages: - if not page.shape: - continue - shape = page.shape + (page.axes, - page.compression in TIFF_DECOMPESSORS) - if shape not in pages: - shapes.append(shape) - pages[shape] = [page] - else: - pages[shape].append(page) - series = [Record(pages=pages[s], - axes=(('I' + s[-2]) - if len(pages[s]) > 1 else s[-2]), - dtype=numpy.dtype(pages[s][0].dtype), - shape=((len(pages[s]), ) + s[:-2] - if len(pages[s]) > 1 else s[:-2])) - for s in shapes] - - # remove empty series, e.g. in MD Gel files - series = [s for s in series if sum(s.shape) > 0] - - return series - - def asarray(self, key=None, series=None, memmap=False): - """Return image data from multiple TIFF pages as numpy array. - - By default the first image series is returned. - - Parameters - ---------- - key : int, slice, or sequence of page indices - Defines which pages to return as array. - series : int - Defines which series of pages to return as array. - memmap : bool - If True, return an array stored in a binary file on disk - if possible. - - """ - if key is None and series is None: - series = 0 - if series is not None: - pages = self.series[series].pages - else: - pages = self.pages - - if key is None: - pass - elif isinstance(key, int): - pages = [pages[key]] - elif isinstance(key, slice): - pages = pages[key] - elif isinstance(key, collections.Iterable): - pages = [pages[k] for k in key] - else: - raise TypeError("key must be an int, slice, or sequence") - - if not len(pages): - raise ValueError("no pages selected") - - if self.is_nih: - if pages[0].is_palette: - result = stack_pages(pages, colormapped=False, squeeze=False) - result = numpy.take(pages[0].color_map, result, axis=1) - result = numpy.swapaxes(result, 0, 1) - else: - result = stack_pages(pages, memmap=memmap, - colormapped=False, squeeze=False) - elif len(pages) == 1: - return pages[0].asarray(memmap=memmap) - elif self.is_ome: - assert not self.is_palette, "color mapping disabled for ome-tiff" - if any(p is None for p in pages): - # zero out missing pages - firstpage = next(p for p in pages if p) - nopage = numpy.zeros_like( - firstpage.asarray(memmap=False)) - s = self.series[series] - if memmap: - with tempfile.NamedTemporaryFile() as fh: - result = numpy.memmap(fh, dtype=s.dtype, shape=s.shape) - result = result.reshape(-1) - else: - result = numpy.empty(s.shape, s.dtype).reshape(-1) - index = 0 - - class KeepOpen: - # keep Tiff files open between consecutive pages - def __init__(self, parent, close): - self.master = parent - self.parent = parent - self._close = close - - def open(self, page): - if self._close and page and page.parent != self.parent: - if self.parent != self.master: - self.parent.filehandle.close() - self.parent = page.parent - self.parent.filehandle.open() - - def close(self): - if self._close and self.parent != self.master: - self.parent.filehandle.close() - - keep = KeepOpen(self, self._multifile_close) - for page in pages: - keep.open(page) - if page: - a = page.asarray(memmap=False, colormapped=False, - reopen=False) - else: - a = nopage - try: - result[index:index + a.size] = a.reshape(-1) - except ValueError as e: - warnings.warn("ome-tiff: %s" % e) - break - index += a.size - keep.close() - else: - result = stack_pages(pages, memmap=memmap) - - if key is None: - try: - result.shape = self.series[series].shape - except ValueError: - try: - warnings.warn("failed to reshape %s to %s" % ( - result.shape, self.series[series].shape)) - # try series of expected shapes - result.shape = (-1,) + self.series[series].shape - except ValueError: - # revert to generic shape - result.shape = (-1,) + pages[0].shape - else: - result.shape = (-1,) + pages[0].shape - return result - - def _omeseries(self): - """Return image series in OME-TIFF file(s).""" - root = etree.fromstring(self.pages[0].tags['image_description'].value) - uuid = root.attrib.get('UUID', None) - self._files = {uuid: self} - dirname = self._fh.dirname - modulo = {} - result = [] - for element in root: - if element.tag.endswith('BinaryOnly'): - warnings.warn("ome-xml: not an ome-tiff master file") - break - if element.tag.endswith('StructuredAnnotations'): - for annot in element: - if not annot.attrib.get('Namespace', - '').endswith('modulo'): - continue - for value in annot: - for modul in value: - for along in modul: - if not along.tag[:-1].endswith('Along'): - continue - axis = along.tag[-1] - newaxis = along.attrib.get('Type', 'other') - newaxis = AXES_LABELS[newaxis] - if 'Start' in along.attrib: - labels = range( - int(along.attrib['Start']), - int(along.attrib['End']) + 1, - int(along.attrib.get('Step', 1))) - else: - labels = [label.text for label in along - if label.tag.endswith('Label')] - modulo[axis] = (newaxis, labels) - if not element.tag.endswith('Image'): - continue - for pixels in element: - if not pixels.tag.endswith('Pixels'): - continue - atr = pixels.attrib - dtype = atr.get('Type', None) - axes = ''.join(reversed(atr['DimensionOrder'])) - shape = list(int(atr['Size'+ax]) for ax in axes) - size = product(shape[:-2]) - ifds = [None] * size - for data in pixels: - if not data.tag.endswith('TiffData'): - continue - atr = data.attrib - ifd = int(atr.get('IFD', 0)) - num = int(atr.get('NumPlanes', 1 if 'IFD' in atr else 0)) - num = int(atr.get('PlaneCount', num)) - idx = [int(atr.get('First'+ax, 0)) for ax in axes[:-2]] - try: - idx = numpy.ravel_multi_index(idx, shape[:-2]) - except ValueError: - # ImageJ produces invalid ome-xml when cropping - warnings.warn("ome-xml: invalid TiffData index") - continue - for uuid in data: - if not uuid.tag.endswith('UUID'): - continue - if uuid.text not in self._files: - if not self._multifile: - # abort reading multifile OME series - # and fall back to generic series - return [] - fname = uuid.attrib['FileName'] - try: - tif = TiffFile(os.path.join(dirname, fname)) - except (IOError, ValueError): - tif.close() - warnings.warn( - "ome-xml: failed to read '%s'" % fname) - break - self._files[uuid.text] = tif - if self._multifile_close: - tif.close() - pages = self._files[uuid.text].pages - try: - for i in range(num if num else len(pages)): - ifds[idx + i] = pages[ifd + i] - except IndexError: - warnings.warn("ome-xml: index out of range") - # only process first uuid - break - else: - pages = self.pages - try: - for i in range(num if num else len(pages)): - ifds[idx + i] = pages[ifd + i] - except IndexError: - warnings.warn("ome-xml: index out of range") - if all(i is None for i in ifds): - # skip images without data - continue - dtype = next(i for i in ifds if i).dtype - result.append(Record(axes=axes, shape=shape, pages=ifds, - dtype=numpy.dtype(dtype))) - - for record in result: - for axis, (newaxis, labels) in modulo.items(): - i = record.axes.index(axis) - size = len(labels) - if record.shape[i] == size: - record.axes = record.axes.replace(axis, newaxis, 1) - else: - record.shape[i] //= size - record.shape.insert(i+1, size) - record.axes = record.axes.replace(axis, axis+newaxis, 1) - record.shape = tuple(record.shape) - - # squeeze dimensions - for record in result: - record.shape, record.axes = squeeze_axes(record.shape, record.axes) - - return result - - def __len__(self): - """Return number of image pages in file.""" - return len(self.pages) - - def __getitem__(self, key): - """Return specified page.""" - return self.pages[key] - - def __iter__(self): - """Return iterator over pages.""" - return iter(self.pages) - - def __str__(self): - """Return string containing information about file.""" - result = [ - self._fh.name.capitalize(), - format_size(self._fh.size), - {'<': 'little endian', '>': 'big endian'}[self.byteorder]] - if self.is_bigtiff: - result.append("bigtiff") - if len(self.pages) > 1: - result.append("%i pages" % len(self.pages)) - if len(self.series) > 1: - result.append("%i series" % len(self.series)) - if len(self._files) > 1: - result.append("%i files" % (len(self._files))) - return ", ".join(result) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - @lazyattr - def fstat(self): - try: - return os.fstat(self._fh.fileno()) - except Exception: # io.UnsupportedOperation - return None - - @lazyattr - def is_bigtiff(self): - return self.offset_size != 4 - - @lazyattr - def is_rgb(self): - return all(p.is_rgb for p in self.pages) - - @lazyattr - def is_palette(self): - return all(p.is_palette for p in self.pages) - - @lazyattr - def is_mdgel(self): - return any(p.is_mdgel for p in self.pages) - - @lazyattr - def is_mediacy(self): - return any(p.is_mediacy for p in self.pages) - - @lazyattr - def is_stk(self): - return all(p.is_stk for p in self.pages) - - @lazyattr - def is_lsm(self): - return self.pages[0].is_lsm - - @lazyattr - def is_imagej(self): - return self.pages[0].is_imagej - - @lazyattr - def is_micromanager(self): - return self.pages[0].is_micromanager - - @lazyattr - def is_nih(self): - return self.pages[0].is_nih - - @lazyattr - def is_fluoview(self): - return self.pages[0].is_fluoview - - @lazyattr - def is_ome(self): - return self.pages[0].is_ome - - -class TiffPage(object): - """A TIFF image file directory (IFD). - - Attributes - ---------- - index : int - Index of page in file. - dtype : str {TIFF_SAMPLE_DTYPES} - Data type of image, colormapped if applicable. - shape : tuple - Dimensions of the image array in TIFF page, - colormapped and with one alpha channel if applicable. - axes : str - Axes label codes: - 'X' width, 'Y' height, 'S' sample, 'I' image series|page|plane, - 'Z' depth, 'C' color|em-wavelength|channel, 'E' ex-wavelength|lambda, - 'T' time, 'R' region|tile, 'A' angle, 'P' phase, 'H' lifetime, - 'L' exposure, 'V' event, 'Q' unknown, '_' missing - tags : TiffTags - Dictionary of tags in page. - Tag values are also directly accessible as attributes. - color_map : numpy array - Color look up table, if exists. - cz_lsm_scan_info: Record(dict) - LSM scan info attributes, if exists. - imagej_tags: Record(dict) - Consolidated ImageJ description and metadata tags, if exists. - uic_tags: Record(dict) - Consolidated MetaMorph STK/UIC tags, if exists. - - All attributes are read-only. - - Notes - ----- - The internal, normalized '_shape' attribute is 6 dimensional: - - 0. number planes (stk) - 1. planar samples_per_pixel - 2. image_depth Z (sgi) - 3. image_length Y - 4. image_width X - 5. contig samples_per_pixel - - """ - def __init__(self, parent): - """Initialize instance from file.""" - self.parent = parent - self.index = len(parent.pages) - self.shape = self._shape = () - self.dtype = self._dtype = None - self.axes = "" - self.tags = TiffTags() - - self._fromfile() - self._process_tags() - - def _fromfile(self): - """Read TIFF IFD structure and its tags from file. - - File cursor must be at storage position of IFD offset and is left at - offset to next IFD. - - Raises StopIteration if offset (first bytes read) is 0. - - """ - fh = self.parent.filehandle - byteorder = self.parent.byteorder - offset_size = self.parent.offset_size - - fmt = {4: 'I', 8: 'Q'}[offset_size] - offset = struct.unpack(byteorder + fmt, fh.read(offset_size))[0] - if not offset: - raise StopIteration() - - # read standard tags - tags = self.tags - fh.seek(offset) - fmt, size = {4: ('H', 2), 8: ('Q', 8)}[offset_size] - try: - numtags = struct.unpack(byteorder + fmt, fh.read(size))[0] - except Exception: - warnings.warn("corrupted page list") - raise StopIteration() - - tagcode = 0 - for _ in range(numtags): - try: - tag = TiffTag(self.parent) - # print(tag) - except TiffTag.Error as e: - warnings.warn(str(e)) - continue - if tagcode > tag.code: - # expected for early LSM and tifffile versions - warnings.warn("tags are not ordered by code") - tagcode = tag.code - if tag.name not in tags: - tags[tag.name] = tag - else: - # some files contain multiple IFD with same code - # e.g. MicroManager files contain two image_description - i = 1 - while True: - name = "%s_%i" % (tag.name, i) - if name not in tags: - tags[name] = tag - break - - pos = fh.tell() - - if self.is_lsm or (self.index and self.parent.is_lsm): - # correct non standard LSM bitspersample tags - self.tags['bits_per_sample']._correct_lsm_bitspersample(self) - - if self.is_lsm: - # read LSM info subrecords - for name, reader in CZ_LSM_INFO_READERS.items(): - try: - offset = self.cz_lsm_info['offset_'+name] - except KeyError: - continue - if offset < 8: - # older LSM revision - continue - fh.seek(offset) - try: - setattr(self, 'cz_lsm_'+name, reader(fh)) - except ValueError: - pass - - elif self.is_stk and 'uic1tag' in tags and not tags['uic1tag'].value: - # read uic1tag now that plane count is known - uic1tag = tags['uic1tag'] - fh.seek(uic1tag.value_offset) - tags['uic1tag'].value = Record( - read_uic1tag(fh, byteorder, uic1tag.dtype, uic1tag.count, - tags['uic2tag'].count)) - fh.seek(pos) - - def _process_tags(self): - """Validate standard tags and initialize attributes. - - Raise ValueError if tag values are not supported. - - """ - tags = self.tags - for code, (name, default, dtype, count, validate) in TIFF_TAGS.items(): - if not (name in tags or default is None): - tags[name] = TiffTag(code, dtype=dtype, count=count, - value=default, name=name) - if name in tags and validate: - try: - if tags[name].count == 1: - setattr(self, name, validate[tags[name].value]) - else: - setattr(self, name, tuple( - validate[value] for value in tags[name].value)) - except KeyError: - raise ValueError("%s.value (%s) not supported" % - (name, tags[name].value)) - - tag = tags['bits_per_sample'] - if tag.count == 1: - self.bits_per_sample = tag.value - else: - # LSM might list more items than samples_per_pixel - value = tag.value[:self.samples_per_pixel] - if any((v-value[0] for v in value)): - self.bits_per_sample = value - else: - self.bits_per_sample = value[0] - - tag = tags['sample_format'] - if tag.count == 1: - self.sample_format = TIFF_SAMPLE_FORMATS[tag.value] - else: - value = tag.value[:self.samples_per_pixel] - if any((v-value[0] for v in value)): - self.sample_format = [TIFF_SAMPLE_FORMATS[v] for v in value] - else: - self.sample_format = TIFF_SAMPLE_FORMATS[value[0]] - - if 'photometric' not in tags: - self.photometric = None - - if 'image_depth' not in tags: - self.image_depth = 1 - - if 'image_length' in tags: - self.strips_per_image = int(math.floor( - float(self.image_length + self.rows_per_strip - 1) / - self.rows_per_strip)) - else: - self.strips_per_image = 0 - - key = (self.sample_format, self.bits_per_sample) - self.dtype = self._dtype = TIFF_SAMPLE_DTYPES.get(key, None) - - if 'image_length' not in self.tags or 'image_width' not in self.tags: - # some GEL file pages are missing image data - self.image_length = 0 - self.image_width = 0 - self.image_depth = 0 - self.strip_offsets = 0 - self._shape = () - self.shape = () - self.axes = '' - - if self.is_palette: - self.dtype = self.tags['color_map'].dtype[1] - self.color_map = numpy.array(self.color_map, self.dtype) - dmax = self.color_map.max() - if dmax < 256: - self.dtype = numpy.uint8 - self.color_map = self.color_map.astype(self.dtype) - #else: - # self.dtype = numpy.uint8 - # self.color_map >>= 8 - # self.color_map = self.color_map.astype(self.dtype) - self.color_map.shape = (3, -1) - - # determine shape of data - image_length = self.image_length - image_width = self.image_width - image_depth = self.image_depth - samples_per_pixel = self.samples_per_pixel - - if self.is_stk: - assert self.image_depth == 1 - planes = self.tags['uic2tag'].count - if self.is_contig: - self._shape = (planes, 1, 1, image_length, image_width, - samples_per_pixel) - if samples_per_pixel == 1: - self.shape = (planes, image_length, image_width) - self.axes = 'YX' - else: - self.shape = (planes, image_length, image_width, - samples_per_pixel) - self.axes = 'YXS' - else: - self._shape = (planes, samples_per_pixel, 1, image_length, - image_width, 1) - if samples_per_pixel == 1: - self.shape = (planes, image_length, image_width) - self.axes = 'YX' - else: - self.shape = (planes, samples_per_pixel, image_length, - image_width) - self.axes = 'SYX' - # detect type of series - if planes == 1: - self.shape = self.shape[1:] - elif numpy.all(self.uic2tag.z_distance != 0): - self.axes = 'Z' + self.axes - elif numpy.all(numpy.diff(self.uic2tag.time_created) != 0): - self.axes = 'T' + self.axes - else: - self.axes = 'I' + self.axes - # DISABLED - if self.is_palette: - assert False, "color mapping disabled for stk" - if self.color_map.shape[1] >= 2**self.bits_per_sample: - if image_depth == 1: - self.shape = (3, planes, image_length, image_width) - else: - self.shape = (3, planes, image_depth, image_length, - image_width) - self.axes = 'C' + self.axes - else: - warnings.warn("palette cannot be applied") - self.is_palette = False - elif self.is_palette: - samples = 1 - if 'extra_samples' in self.tags: - samples += len(self.extra_samples) - if self.is_contig: - self._shape = (1, 1, image_depth, image_length, image_width, - samples) - else: - self._shape = (1, samples, image_depth, image_length, - image_width, 1) - if self.color_map.shape[1] >= 2**self.bits_per_sample: - if image_depth == 1: - self.shape = (3, image_length, image_width) - self.axes = 'CYX' - else: - self.shape = (3, image_depth, image_length, image_width) - self.axes = 'CZYX' - else: - warnings.warn("palette cannot be applied") - self.is_palette = False - if image_depth == 1: - self.shape = (image_length, image_width) - self.axes = 'YX' - else: - self.shape = (image_depth, image_length, image_width) - self.axes = 'ZYX' - elif self.is_rgb or samples_per_pixel > 1: - if self.is_contig: - self._shape = (1, 1, image_depth, image_length, image_width, - samples_per_pixel) - if image_depth == 1: - self.shape = (image_length, image_width, samples_per_pixel) - self.axes = 'YXS' - else: - self.shape = (image_depth, image_length, image_width, - samples_per_pixel) - self.axes = 'ZYXS' - else: - self._shape = (1, samples_per_pixel, image_depth, - image_length, image_width, 1) - if image_depth == 1: - self.shape = (samples_per_pixel, image_length, image_width) - self.axes = 'SYX' - else: - self.shape = (samples_per_pixel, image_depth, - image_length, image_width) - self.axes = 'SZYX' - if False and self.is_rgb and 'extra_samples' in self.tags: - # DISABLED: only use RGB and first alpha channel if exists - extra_samples = self.extra_samples - if self.tags['extra_samples'].count == 1: - extra_samples = (extra_samples, ) - for exs in extra_samples: - if exs in ('unassalpha', 'assocalpha', 'unspecified'): - if self.is_contig: - self.shape = self.shape[:-1] + (4,) - else: - self.shape = (4,) + self.shape[1:] - break - else: - self._shape = (1, 1, image_depth, image_length, image_width, 1) - if image_depth == 1: - self.shape = (image_length, image_width) - self.axes = 'YX' - else: - self.shape = (image_depth, image_length, image_width) - self.axes = 'ZYX' - if not self.compression and 'strip_byte_counts' not in tags: - self.strip_byte_counts = ( - product(self.shape) * (self.bits_per_sample // 8), ) - - assert len(self.shape) == len(self.axes) - - def asarray(self, squeeze=True, colormapped=True, rgbonly=False, - scale_mdgel=False, memmap=False, reopen=True): - """Read image data from file and return as numpy array. - - Raise ValueError if format is unsupported. - If any of 'squeeze', 'colormapped', or 'rgbonly' are not the default, - the shape of the returned array might be different from the page shape. - - Parameters - ---------- - squeeze : bool - If True, all length-1 dimensions (except X and Y) are - squeezed out from result. - colormapped : bool - If True, color mapping is applied for palette-indexed images. - rgbonly : bool - If True, return RGB(A) image without additional extra samples. - memmap : bool - If True, use numpy.memmap to read arrays from file if possible. - For use on 64 bit systems and files with few huge contiguous data. - reopen : bool - If True and the parent file handle is closed, the file is - temporarily re-opened (and closed if no exception occurs). - scale_mdgel : bool - If True, MD Gel data will be scaled according to the private - metadata in the second TIFF page. The dtype will be float32. - - """ - if not self._shape: - return - - if self.dtype is None: - raise ValueError("data type not supported: %s%i" % ( - self.sample_format, self.bits_per_sample)) - if self.compression not in TIFF_DECOMPESSORS: - raise ValueError("cannot decompress %s" % self.compression) - tag = self.tags['sample_format'] - if tag.count != 1 and any((i-tag.value[0] for i in tag.value)): - raise ValueError("sample formats don't match %s" % str(tag.value)) - - fh = self.parent.filehandle - closed = fh.closed - if closed: - if reopen: - fh.open() - else: - raise IOError("file handle is closed") - - dtype = self._dtype - shape = self._shape - image_width = self.image_width - image_length = self.image_length - image_depth = self.image_depth - typecode = self.parent.byteorder + dtype - bits_per_sample = self.bits_per_sample - - if self.is_tiled: - if 'tile_offsets' in self.tags: - byte_counts = self.tile_byte_counts - offsets = self.tile_offsets - else: - byte_counts = self.strip_byte_counts - offsets = self.strip_offsets - tile_width = self.tile_width - tile_length = self.tile_length - tile_depth = self.tile_depth if 'tile_depth' in self.tags else 1 - tw = (image_width + tile_width - 1) // tile_width - tl = (image_length + tile_length - 1) // tile_length - td = (image_depth + tile_depth - 1) // tile_depth - shape = (shape[0], shape[1], - td*tile_depth, tl*tile_length, tw*tile_width, shape[-1]) - tile_shape = (tile_depth, tile_length, tile_width, shape[-1]) - runlen = tile_width - else: - byte_counts = self.strip_byte_counts - offsets = self.strip_offsets - runlen = image_width - - if any(o < 2 for o in offsets): - raise ValueError("corrupted page") - - if memmap and self._is_memmappable(rgbonly, colormapped): - result = fh.memmap_array(typecode, shape, offset=offsets[0]) - elif self.is_contiguous: - fh.seek(offsets[0]) - result = fh.read_array(typecode, product(shape)) - result = result.astype('=' + dtype) - else: - if self.is_contig: - runlen *= self.samples_per_pixel - if bits_per_sample in (8, 16, 32, 64, 128): - if (bits_per_sample * runlen) % 8: - raise ValueError("data and sample size mismatch") - - def unpack(x): - try: - return numpy.fromstring(x, typecode) - except ValueError as e: - # strips may be missing EOI - warnings.warn("unpack: %s" % e) - xlen = ((len(x) // (bits_per_sample // 8)) - * (bits_per_sample // 8)) - return numpy.fromstring(x[:xlen], typecode) - - elif isinstance(bits_per_sample, tuple): - def unpack(x): - return unpackrgb(x, typecode, bits_per_sample) - else: - def unpack(x): - return unpackints(x, typecode, bits_per_sample, runlen) - - decompress = TIFF_DECOMPESSORS[self.compression] - if self.compression == 'jpeg': - table = self.jpeg_tables if 'jpeg_tables' in self.tags else b'' - decompress = lambda x: decodejpg(x, table, self.photometric) - - if self.is_tiled: - result = numpy.empty(shape, dtype) - tw, tl, td, pl = 0, 0, 0, 0 - for offset, bytecount in zip(offsets, byte_counts): - fh.seek(offset) - tile = unpack(decompress(fh.read(bytecount))) - tile.shape = tile_shape - if self.predictor == 'horizontal': - numpy.cumsum(tile, axis=-2, dtype=dtype, out=tile) - result[0, pl, td:td+tile_depth, - tl:tl+tile_length, tw:tw+tile_width, :] = tile - del tile - tw += tile_width - if tw >= shape[4]: - tw, tl = 0, tl + tile_length - if tl >= shape[3]: - tl, td = 0, td + tile_depth - if td >= shape[2]: - td, pl = 0, pl + 1 - result = result[..., - :image_depth, :image_length, :image_width, :] - else: - strip_size = (self.rows_per_strip * self.image_width * - self.samples_per_pixel) - result = numpy.empty(shape, dtype).reshape(-1) - index = 0 - for offset, bytecount in zip(offsets, byte_counts): - fh.seek(offset) - strip = fh.read(bytecount) - strip = decompress(strip) - strip = unpack(strip) - size = min(result.size, strip.size, strip_size, - result.size - index) - result[index:index+size] = strip[:size] - del strip - index += size - - result.shape = self._shape - - if self.predictor == 'horizontal' and not (self.is_tiled and not - self.is_contiguous): - # work around bug in LSM510 software - if not (self.parent.is_lsm and not self.compression): - numpy.cumsum(result, axis=-2, dtype=dtype, out=result) - - if colormapped and self.is_palette: - if self.color_map.shape[1] >= 2**bits_per_sample: - # FluoView and LSM might fail here - result = numpy.take(self.color_map, - result[:, 0, :, :, :, 0], axis=1) - elif rgbonly and self.is_rgb and 'extra_samples' in self.tags: - # return only RGB and first alpha channel if exists - extra_samples = self.extra_samples - if self.tags['extra_samples'].count == 1: - extra_samples = (extra_samples, ) - for i, exs in enumerate(extra_samples): - if exs in ('unassalpha', 'assocalpha', 'unspecified'): - if self.is_contig: - result = result[..., [0, 1, 2, 3+i]] - else: - result = result[:, [0, 1, 2, 3+i]] - break - else: - if self.is_contig: - result = result[..., :3] - else: - result = result[:, :3] - - if squeeze: - try: - result.shape = self.shape - except ValueError: - warnings.warn("failed to reshape from %s to %s" % ( - str(result.shape), str(self.shape))) - - if scale_mdgel and self.parent.is_mdgel: - # MD Gel stores private metadata in the second page - tags = self.parent.pages[1] - if tags.md_file_tag in (2, 128): - scale = tags.md_scale_pixel - scale = scale[0] / scale[1] # rational - result = result.astype('float32') - if tags.md_file_tag == 2: - result **= 2 # squary root data format - result *= scale - - if closed: - # TODO: file remains open if an exception occurred above - fh.close() - return result - - def _is_memmappable(self, rgbonly, colormapped): - """Return if image data in file can be memory mapped.""" - if not self.parent.filehandle.is_file or not self.is_contiguous: - return False - return not (self.predictor or - (rgbonly and 'extra_samples' in self.tags) or - (colormapped and self.is_palette) or - ({'big': '>', 'little': '<'}[sys.byteorder] != - self.parent.byteorder)) - - @lazyattr - def is_contiguous(self): - """Return offset and size of contiguous data, else None. - - Excludes prediction and colormapping. - - """ - if self.compression or self.bits_per_sample not in (8, 16, 32, 64): - return - if self.is_tiled: - if (self.image_width != self.tile_width or - self.image_length % self.tile_length or - self.tile_width % 16 or self.tile_length % 16): - return - if ('image_depth' in self.tags and 'tile_depth' in self.tags and - (self.image_length != self.tile_length or - self.image_depth % self.tile_depth)): - return - offsets = self.tile_offsets - byte_counts = self.tile_byte_counts - else: - offsets = self.strip_offsets - byte_counts = self.strip_byte_counts - if len(offsets) == 1: - return offsets[0], byte_counts[0] - if self.is_stk or all(offsets[i] + byte_counts[i] == offsets[i+1] - or byte_counts[i+1] == 0 # no data/ignore offset - for i in range(len(offsets)-1)): - return offsets[0], sum(byte_counts) - - def __str__(self): - """Return string containing information about page.""" - s = ', '.join(s for s in ( - ' x '.join(str(i) for i in self.shape), - str(numpy.dtype(self.dtype)), - '%s bit' % str(self.bits_per_sample), - self.photometric if 'photometric' in self.tags else '', - self.compression if self.compression else 'raw', - '|'.join(t[3:] for t in ( - 'is_stk', 'is_lsm', 'is_nih', 'is_ome', 'is_imagej', - 'is_micromanager', 'is_fluoview', 'is_mdgel', 'is_mediacy', - 'is_sgi', 'is_reduced', 'is_tiled', - 'is_contiguous') if getattr(self, t))) if s) - return "Page %i: %s" % (self.index, s) - - def __getattr__(self, name): - """Return tag value.""" - if name in self.tags: - value = self.tags[name].value - setattr(self, name, value) - return value - raise AttributeError(name) - - @lazyattr - def uic_tags(self): - """Consolidate UIC tags.""" - if not self.is_stk: - raise AttributeError("uic_tags") - tags = self.tags - result = Record() - result.number_planes = tags['uic2tag'].count - if 'image_description' in tags: - result.plane_descriptions = self.image_description.split(b'\x00') - if 'uic1tag' in tags: - result.update(tags['uic1tag'].value) - if 'uic3tag' in tags: - result.update(tags['uic3tag'].value) # wavelengths - if 'uic4tag' in tags: - result.update(tags['uic4tag'].value) # override uic1 tags - uic2tag = tags['uic2tag'].value - result.z_distance = uic2tag.z_distance - result.time_created = uic2tag.time_created - result.time_modified = uic2tag.time_modified - try: - result.datetime_created = [ - julian_datetime(*dt) for dt in - zip(uic2tag.date_created, uic2tag.time_created)] - result.datetime_modified = [ - julian_datetime(*dt) for dt in - zip(uic2tag.date_modified, uic2tag.time_modified)] - except ValueError as e: - warnings.warn("uic_tags: %s" % e) - return result - - @lazyattr - def imagej_tags(self): - """Consolidate ImageJ metadata.""" - if not self.is_imagej: - raise AttributeError("imagej_tags") - tags = self.tags - if 'image_description_1' in tags: - # MicroManager - result = imagej_description(tags['image_description_1'].value) - else: - result = imagej_description(tags['image_description'].value) - if 'imagej_metadata' in tags: - try: - result.update(imagej_metadata( - tags['imagej_metadata'].value, - tags['imagej_byte_counts'].value, - self.parent.byteorder)) - except Exception as e: - warnings.warn(str(e)) - return Record(result) - - @lazyattr - def is_rgb(self): - """True if page contains a RGB image.""" - return ('photometric' in self.tags and - self.tags['photometric'].value == 2) - - @lazyattr - def is_contig(self): - """True if page contains a contiguous image.""" - return ('planar_configuration' in self.tags and - self.tags['planar_configuration'].value == 1) - - @lazyattr - def is_palette(self): - """True if page contains a palette-colored image and not OME or STK.""" - try: - # turn off color mapping for OME-TIFF and STK - if self.is_stk or self.is_ome or self.parent.is_ome: - return False - except IndexError: - pass # OME-XML not found in first page - return ('photometric' in self.tags and - self.tags['photometric'].value == 3) - - @lazyattr - def is_tiled(self): - """True if page contains tiled image.""" - return 'tile_width' in self.tags - - @lazyattr - def is_reduced(self): - """True if page is a reduced image of another image.""" - return bool(self.tags['new_subfile_type'].value & 1) - - @lazyattr - def is_mdgel(self): - """True if page contains md_file_tag tag.""" - return 'md_file_tag' in self.tags - - @lazyattr - def is_mediacy(self): - """True if page contains Media Cybernetics Id tag.""" - return ('mc_id' in self.tags and - self.tags['mc_id'].value.startswith(b'MC TIFF')) - - @lazyattr - def is_stk(self): - """True if page contains UIC2Tag tag.""" - return 'uic2tag' in self.tags - - @lazyattr - def is_lsm(self): - """True if page contains LSM CZ_LSM_INFO tag.""" - return 'cz_lsm_info' in self.tags - - @lazyattr - def is_fluoview(self): - """True if page contains FluoView MM_STAMP tag.""" - return 'mm_stamp' in self.tags - - @lazyattr - def is_nih(self): - """True if page contains NIH image header.""" - return 'nih_image_header' in self.tags - - @lazyattr - def is_sgi(self): - """True if page contains SGI image and tile depth tags.""" - return 'image_depth' in self.tags and 'tile_depth' in self.tags - - @lazyattr - def is_ome(self): - """True if page contains OME-XML in image_description tag.""" - return ('image_description' in self.tags and self.tags[ - 'image_description'].value.startswith(b' parent.offset_size or code in CUSTOM_TAGS: - pos = fh.tell() - tof = {4: 'I', 8: 'Q'}[parent.offset_size] - self.value_offset = offset = struct.unpack(byteorder+tof, value)[0] - if offset < 0 or offset > parent.filehandle.size: - raise TiffTag.Error("corrupt file - invalid tag value offset") - elif offset < 4: - raise TiffTag.Error("corrupt value offset for tag %i" % code) - fh.seek(offset) - if code in CUSTOM_TAGS: - readfunc = CUSTOM_TAGS[code][1] - value = readfunc(fh, byteorder, dtype, count) - if isinstance(value, dict): # numpy.core.records.record - value = Record(value) - elif code in TIFF_TAGS or dtype[-1] == 's': - value = struct.unpack(fmt, fh.read(size)) - else: - value = read_numpy(fh, byteorder, dtype, count) - fh.seek(pos) - else: - value = struct.unpack(fmt, value[:size]) - - if code not in CUSTOM_TAGS and code not in (273, 279, 324, 325): - # scalar value if not strip/tile offsets/byte_counts - if len(value) == 1: - value = value[0] - - if (dtype.endswith('s') and isinstance(value, bytes) - and self._type != 7): - # TIFF ASCII fields can contain multiple strings, - # each terminated with a NUL - value = stripascii(value) - - self.code = code - self.name = name - self.dtype = dtype - self.count = count - self.value = value - - def _correct_lsm_bitspersample(self, parent): - """Correct LSM bitspersample tag. - - Old LSM writers may use a separate region for two 16-bit values, - although they fit into the tag value element of the tag. - - """ - if self.code == 258 and self.count == 2: - # TODO: test this. Need example file. - warnings.warn("correcting LSM bitspersample tag") - fh = parent.filehandle - tof = {4: '') - - def __str__(self): - """Return string containing information about tag.""" - return ' '.join(str(getattr(self, s)) for s in self.__slots__) - - -class TiffSequence(object): - """Sequence of image files. - - The data shape and dtype of all files must match. - - Properties - ---------- - files : list - List of file names. - shape : tuple - Shape of image sequence. - axes : str - Labels of axes in shape. - - Examples - -------- - >>> tifs = TiffSequence("test.oif.files/*.tif") - >>> tifs.shape, tifs.axes - ((2, 100), 'CT') - >>> data = tifs.asarray() - >>> data.shape - (2, 100, 256, 256) - - """ - _patterns = { - 'axes': r""" - # matches Olympus OIF and Leica TIFF series - _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4})) - _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? - _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? - _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? - _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? - _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? - _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? - """} - - class ParseError(Exception): - pass - - def __init__(self, files, imread=TiffFile, pattern='axes', - *args, **kwargs): - """Initialize instance from multiple files. - - Parameters - ---------- - files : str, or sequence of str - Glob pattern or sequence of file names. - imread : function or class - Image read function or class with asarray function returning numpy - array from single file. - pattern : str - Regular expression pattern that matches axes names and sequence - indices in file names. - By default this matches Olympus OIF and Leica TIFF series. - - """ - if isinstance(files, basestring): - files = natural_sorted(glob.glob(files)) - files = list(files) - if not files: - raise ValueError("no files found") - #if not os.path.isfile(files[0]): - # raise ValueError("file not found") - self.files = files - - if hasattr(imread, 'asarray'): - # redefine imread - _imread = imread - - def imread(fname, *args, **kwargs): - with _imread(fname) as im: - return im.asarray(*args, **kwargs) - - self.imread = imread - - self.pattern = self._patterns.get(pattern, pattern) - try: - self._parse() - if not self.axes: - self.axes = 'I' - except self.ParseError: - self.axes = 'I' - self.shape = (len(files),) - self._start_index = (0,) - self._indices = tuple((i,) for i in range(len(files))) - - def __str__(self): - """Return string with information about image sequence.""" - return "\n".join([ - self.files[0], - '* files: %i' % len(self.files), - '* axes: %s' % self.axes, - '* shape: %s' % str(self.shape)]) - - def __len__(self): - return len(self.files) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - def close(self): - pass - - def asarray(self, memmap=False, *args, **kwargs): - """Read image data from all files and return as single numpy array. - - If memmap is True, return an array stored in a binary file on disk. - The args and kwargs parameters are passed to the imread function. - - Raise IndexError or ValueError if image shapes don't match. - - """ - im = self.imread(self.files[0], *args, **kwargs) - shape = self.shape + im.shape - if memmap: - with tempfile.NamedTemporaryFile() as fh: - result = numpy.memmap(fh, dtype=im.dtype, shape=shape) - else: - result = numpy.zeros(shape, dtype=im.dtype) - result = result.reshape(-1, *im.shape) - for index, fname in zip(self._indices, self.files): - index = [i-j for i, j in zip(index, self._start_index)] - index = numpy.ravel_multi_index(index, self.shape) - im = self.imread(fname, *args, **kwargs) - result[index] = im - result.shape = shape - return result - - def _parse(self): - """Get axes and shape from file names.""" - if not self.pattern: - raise self.ParseError("invalid pattern") - pattern = re.compile(self.pattern, re.IGNORECASE | re.VERBOSE) - matches = pattern.findall(self.files[0]) - if not matches: - raise self.ParseError("pattern doesn't match file names") - matches = matches[-1] - if len(matches) % 2: - raise self.ParseError("pattern doesn't match axis name and index") - axes = ''.join(m for m in matches[::2] if m) - if not axes: - raise self.ParseError("pattern doesn't match file names") - - indices = [] - for fname in self.files: - matches = pattern.findall(fname)[-1] - if axes != ''.join(m for m in matches[::2] if m): - raise ValueError("axes don't match within the image sequence") - indices.append([int(m) for m in matches[1::2] if m]) - shape = tuple(numpy.max(indices, axis=0)) - start_index = tuple(numpy.min(indices, axis=0)) - shape = tuple(i-j+1 for i, j in zip(shape, start_index)) - if product(shape) != len(self.files): - warnings.warn("files are missing. Missing data are zeroed") - - self.axes = axes.upper() - self.shape = shape - self._indices = indices - self._start_index = start_index - - -class Record(dict): - """Dictionary with attribute access. - - Can also be initialized with numpy.core.records.record. - - """ - __slots__ = () - - def __init__(self, arg=None, **kwargs): - if kwargs: - arg = kwargs - elif arg is None: - arg = {} - try: - dict.__init__(self, arg) - except (TypeError, ValueError): - for i, name in enumerate(arg.dtype.names): - v = arg[i] - self[name] = v if v.dtype.char != 'S' else stripnull(v) - - def __getattr__(self, name): - return self[name] - - def __setattr__(self, name, value): - self.__setitem__(name, value) - - def __str__(self): - """Pretty print Record.""" - s = [] - lists = [] - for k in sorted(self): - try: - if k.startswith('_'): # does not work with byte - continue - except AttributeError: - pass - v = self[k] - if isinstance(v, (list, tuple)) and len(v): - if isinstance(v[0], Record): - lists.append((k, v)) - continue - elif isinstance(v[0], TiffPage): - v = [i.index for i in v if i] - s.append( - ("* %s: %s" % (k, str(v))).split("\n", 1)[0] - [:PRINT_LINE_LEN].rstrip()) - for k, v in lists: - l = [] - for i, w in enumerate(v): - l.append("* %s[%i]\n %s" % (k, i, - str(w).replace("\n", "\n "))) - s.append('\n'.join(l)) - return '\n'.join(s) - - -class TiffTags(Record): - """Dictionary of TiffTag with attribute access.""" - - def __str__(self): - """Return string with information about all tags.""" - s = [] - for tag in sorted(self.values(), key=lambda x: x.code): - typecode = "%i%s" % (tag.count * int(tag.dtype[0]), tag.dtype[1]) - line = "* %i %s (%s) %s" % ( - tag.code, tag.name, typecode, tag.as_str()) - s.append(line[:PRINT_LINE_LEN].lstrip()) - return '\n'.join(s) - - -class FileHandle(object): - """Binary file handle. - - * Handle embedded files (for CZI within CZI files). - * Allow to re-open closed files (for multi file formats such as OME-TIFF). - * Read numpy arrays and records from file like objects. - - Only binary read, seek, tell, and close are supported on embedded files. - When initialized from another file handle, do not use it unless this - FileHandle is closed. - - Attributes - ---------- - name : str - Name of the file. - path : str - Absolute path to file. - size : int - Size of file in bytes. - is_file : bool - If True, file has a filno and can be memory mapped. - - All attributes are read-only. - - """ - __slots__ = ('_fh', '_arg', '_mode', '_name', '_dir', - '_offset', '_size', '_close', 'is_file') - - def __init__(self, arg, mode='rb', name=None, offset=None, size=None): - """Initialize file handle from file name or another file handle. - - Parameters - ---------- - arg : str, File, or FileHandle - File name or open file handle. - mode : str - File open mode in case 'arg' is a file name. - name : str - Optional name of file in case 'arg' is a file handle. - offset : int - Optional start position of embedded file. By default this is - the current file position. - size : int - Optional size of embedded file. By default this is the number - of bytes from the 'offset' to the end of the file. - - """ - self._fh = None - self._arg = arg - self._mode = mode - self._name = name - self._dir = '' - self._offset = offset - self._size = size - self._close = True - self.is_file = False - self.open() - - def open(self): - """Open or re-open file.""" - if self._fh: - return # file is open - - if isinstance(self._arg, basestring): - # file name - self._arg = os.path.abspath(self._arg) - self._dir, self._name = os.path.split(self._arg) - self._fh = open(self._arg, self._mode) - self._close = True - if self._offset is None: - self._offset = 0 - elif isinstance(self._arg, FileHandle): - # FileHandle - self._fh = self._arg._fh - if self._offset is None: - self._offset = 0 - self._offset += self._arg._offset - self._close = False - if not self._name: - if self._offset: - name, ext = os.path.splitext(self._arg._name) - self._name = "%s@%i%s" % (name, self._offset, ext) - else: - self._name = self._arg._name - self._dir = self._arg._dir - else: - # open file object - self._fh = self._arg - if self._offset is None: - self._offset = self._arg.tell() - self._close = False - if not self._name: - try: - self._dir, self._name = os.path.split(self._fh.name) - except AttributeError: - self._name = "Unnamed stream" - - if self._offset: - self._fh.seek(self._offset) - - if self._size is None: - pos = self._fh.tell() - self._fh.seek(self._offset, 2) - self._size = self._fh.tell() - self._fh.seek(pos) - - try: - self._fh.fileno() - self.is_file = True - except Exception: - self.is_file = False - - def read(self, size=-1): - """Read 'size' bytes from file, or until EOF is reached.""" - if size < 0 and self._offset: - size = self._size - return self._fh.read(size) - - def memmap_array(self, dtype, shape, offset=0, mode='r', order='C'): - """Return numpy.memmap of data stored in file.""" - if not self.is_file: - raise ValueError("Can not memory map file without fileno.") - return numpy.memmap(self._fh, dtype=dtype, mode=mode, - offset=self._offset + offset, - shape=shape, order=order) - - def read_array(self, dtype, count=-1, sep=""): - """Return numpy array from file. - - Work around numpy issue #2230, "numpy.fromfile does not accept - StringIO object" https://github.com/numpy/numpy/issues/2230. - - """ - try: - return numpy.fromfile(self._fh, dtype, count, sep) - except IOError: - if count < 0: - size = self._size - else: - size = count * numpy.dtype(dtype).itemsize - data = self._fh.read(size) - return numpy.fromstring(data, dtype, count, sep) - - def read_record(self, dtype, shape=1, byteorder=None): - """Return numpy record from file.""" - try: - rec = numpy.rec.fromfile(self._fh, dtype, shape, - byteorder=byteorder) - except Exception: - dtype = numpy.dtype(dtype) - if shape is None: - shape = self._size // dtype.itemsize - size = product(sequence(shape)) * dtype.itemsize - data = self._fh.read(size) - return numpy.rec.fromstring(data, dtype, shape, - byteorder=byteorder) - return rec[0] if shape == 1 else rec - - def tell(self): - """Return file's current position.""" - return self._fh.tell() - self._offset - - def seek(self, offset, whence=0): - """Set file's current position.""" - if self._offset: - if whence == 0: - self._fh.seek(self._offset + offset, whence) - return - elif whence == 2: - self._fh.seek(self._offset + self._size + offset, 0) - return - self._fh.seek(offset, whence) - - def close(self): - """Close file.""" - if self._close and self._fh: - self._fh.close() - self._fh = None - self.is_file = False - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - def __getattr__(self, name): - """Return attribute from underlying file object.""" - if self._offset: - warnings.warn( - "FileHandle: '%s' not implemented for embedded files" % name) - return getattr(self._fh, name) - - @property - def name(self): - return self._name - - @property - def dirname(self): - return self._dir - - @property - def path(self): - return os.path.join(self._dir, self._name) - - @property - def size(self): - return self._size - - @property - def closed(self): - return self._fh is None - - -def read_bytes(fh, byteorder, dtype, count): - """Read tag data from file and return as byte string.""" - dtype = 'b' if dtype[-1] == 's' else byteorder+dtype[-1] - return fh.read_array(dtype, count).tostring() - - -def read_numpy(fh, byteorder, dtype, count): - """Read tag data from file and return as numpy array.""" - dtype = 'b' if dtype[-1] == 's' else byteorder+dtype[-1] - return fh.read_array(dtype, count) - - -def read_json(fh, byteorder, dtype, count): - """Read JSON tag data from file and return as object.""" - data = fh.read(count) - try: - return json.loads(unicode(stripnull(data), 'utf-8')) - except ValueError: - warnings.warn("invalid JSON `%s`" % data) - - -def read_mm_header(fh, byteorder, dtype, count): - """Read MM_HEADER tag from file and return as numpy.rec.array.""" - return fh.read_record(MM_HEADER, byteorder=byteorder) - - -def read_mm_stamp(fh, byteorder, dtype, count): - """Read MM_STAMP tag from file and return as numpy.array.""" - return fh.read_array(byteorder+'f8', 8) - - -def read_uic1tag(fh, byteorder, dtype, count, plane_count=None): - """Read MetaMorph STK UIC1Tag from file and return as dictionary. - - Return empty dictionary if plane_count is unknown. - - """ - assert dtype in ('2I', '1I') and byteorder == '<' - result = {} - if dtype == '2I': - # pre MetaMorph 2.5 (not tested) - values = fh.read_array(' structure_size: - break - cz_lsm_info.append((name, dtype)) - else: - cz_lsm_info = CZ_LSM_INFO - - return fh.read_record(cz_lsm_info, byteorder=byteorder) - - -def read_cz_lsm_floatpairs(fh): - """Read LSM sequence of float pairs from file and return as list.""" - size = struct.unpack(' 0: - esize, etime, etype = struct.unpack(''}[fh.read(2)] - except IndexError: - raise ValueError("not a MicroManager TIFF file") - - results = {} - fh.seek(8) - (index_header, index_offset, display_header, display_offset, - comments_header, comments_offset, summary_header, summary_length - ) = struct.unpack(byteorder + "IIIIIIII", fh.read(32)) - - if summary_header != 2355492: - raise ValueError("invalid MicroManager summary_header") - results['summary'] = read_json(fh, byteorder, None, summary_length) - - if index_header != 54773648: - raise ValueError("invalid MicroManager index_header") - fh.seek(index_offset) - header, count = struct.unpack(byteorder + "II", fh.read(8)) - if header != 3453623: - raise ValueError("invalid MicroManager index_header") - data = struct.unpack(byteorder + "IIIII"*count, fh.read(20*count)) - results['index_map'] = { - 'channel': data[::5], 'slice': data[1::5], 'frame': data[2::5], - 'position': data[3::5], 'offset': data[4::5]} - - if display_header != 483765892: - raise ValueError("invalid MicroManager display_header") - fh.seek(display_offset) - header, count = struct.unpack(byteorder + "II", fh.read(8)) - if header != 347834724: - raise ValueError("invalid MicroManager display_header") - results['display_settings'] = read_json(fh, byteorder, None, count) - - if comments_header != 99384722: - raise ValueError("invalid MicroManager comments_header") - fh.seek(comments_offset) - header, count = struct.unpack(byteorder + "II", fh.read(8)) - if header != 84720485: - raise ValueError("invalid MicroManager comments_header") - results['comments'] = read_json(fh, byteorder, None, count) - - return results - - -def imagej_metadata(data, bytecounts, byteorder): - """Return dict from ImageJ metadata tag value.""" - _str = str if sys.version_info[0] < 3 else lambda x: str(x, 'cp1252') - - def read_string(data, byteorder): - return _str(stripnull(data[0 if byteorder == '<' else 1::2])) - - def read_double(data, byteorder): - return struct.unpack(byteorder+('d' * (len(data) // 8)), data) - - def read_bytes(data, byteorder): - #return struct.unpack('b' * len(data), data) - return numpy.fromstring(data, 'uint8') - - metadata_types = { # big endian - b'info': ('info', read_string), - b'labl': ('labels', read_string), - b'rang': ('ranges', read_double), - b'luts': ('luts', read_bytes), - b'roi ': ('roi', read_bytes), - b'over': ('overlays', read_bytes)} - metadata_types.update( # little endian - dict((k[::-1], v) for k, v in metadata_types.items())) - - if not bytecounts: - raise ValueError("no ImageJ metadata") - - if not data[:4] in (b'IJIJ', b'JIJI'): - raise ValueError("invalid ImageJ metadata") - - header_size = bytecounts[0] - if header_size < 12 or header_size > 804: - raise ValueError("invalid ImageJ metadata header size") - - ntypes = (header_size - 4) // 8 - header = struct.unpack(byteorder+'4sI'*ntypes, data[4:4+ntypes*8]) - pos = 4 + ntypes * 8 - counter = 0 - result = {} - for mtype, count in zip(header[::2], header[1::2]): - values = [] - name, func = metadata_types.get(mtype, (_str(mtype), read_bytes)) - for _ in range(count): - counter += 1 - pos1 = pos + bytecounts[counter] - values.append(func(data[pos:pos1], byteorder)) - pos = pos1 - result[name.strip()] = values[0] if count == 1 else values - return result - - -def imagej_description(description): - """Return dict from ImageJ image_description tag.""" - def _bool(val): - return {b'true': True, b'false': False}[val.lower()] - - _str = str if sys.version_info[0] < 3 else lambda x: str(x, 'cp1252') - result = {} - for line in description.splitlines(): - try: - key, val = line.split(b'=') - except Exception: - continue - key = key.strip() - val = val.strip() - for dtype in (int, float, _bool, _str): - try: - val = dtype(val) - break - except Exception: - pass - result[_str(key)] = val - return result - - -def _replace_by(module_function, package=None, warn=False): - """Try replace decorated function by module.function.""" - try: - from importlib import import_module - except ImportError: - warnings.warn('could not import module importlib') - return lambda func: func - - def decorate(func, module_function=module_function, warn=warn): - try: - module, function = module_function.split('.') - if not package: - module = import_module(module) - else: - module = import_module('.' + module, package=package) - func, oldfunc = getattr(module, function), func - globals()['__old_' + func.__name__] = oldfunc - except Exception: - if warn: - warnings.warn("failed to import %s" % module_function) - return func - - return decorate - - -def decodejpg(encoded, tables=b'', photometric=None, - ycbcr_subsampling=None, ycbcr_positioning=None): - """Decode JPEG encoded byte string (using _czifile extension module).""" - import _czifile - image = _czifile.decodejpg(encoded, tables) - if photometric == 'rgb' and ycbcr_subsampling and ycbcr_positioning: - # TODO: convert YCbCr to RGB - pass - return image.tostring() - - -@_replace_by('_tifffile.decodepackbits') -def decodepackbits(encoded): - """Decompress PackBits encoded byte string. - - PackBits is a simple byte-oriented run-length compression scheme. - - """ - func = ord if sys.version[0] == '2' else lambda x: x - result = [] - result_extend = result.extend - i = 0 - try: - while True: - n = func(encoded[i]) + 1 - i += 1 - if n < 129: - result_extend(encoded[i:i+n]) - i += n - elif n > 129: - result_extend(encoded[i:i+1] * (258-n)) - i += 1 - except IndexError: - pass - return b''.join(result) if sys.version[0] == '2' else bytes(result) - - -@_replace_by('_tifffile.decodelzw') -def decodelzw(encoded): - """Decompress LZW (Lempel-Ziv-Welch) encoded TIFF strip (byte string). - - The strip must begin with a CLEAR code and end with an EOI code. - - This is an implementation of the LZW decoding algorithm described in (1). - It is not compatible with old style LZW compressed files like quad-lzw.tif. - - """ - len_encoded = len(encoded) - bitcount_max = len_encoded * 8 - unpack = struct.unpack - - if sys.version[0] == '2': - newtable = [chr(i) for i in range(256)] - else: - newtable = [bytes([i]) for i in range(256)] - newtable.extend((0, 0)) - - def next_code(): - """Return integer of `bitw` bits at `bitcount` position in encoded.""" - start = bitcount // 8 - s = encoded[start:start+4] - try: - code = unpack('>I', s)[0] - except Exception: - code = unpack('>I', s + b'\x00'*(4-len(s)))[0] - code <<= bitcount % 8 - code &= mask - return code >> shr - - switchbitch = { # code: bit-width, shr-bits, bit-mask - 255: (9, 23, int(9*'1'+'0'*23, 2)), - 511: (10, 22, int(10*'1'+'0'*22, 2)), - 1023: (11, 21, int(11*'1'+'0'*21, 2)), - 2047: (12, 20, int(12*'1'+'0'*20, 2)), } - bitw, shr, mask = switchbitch[255] - bitcount = 0 - - if len_encoded < 4: - raise ValueError("strip must be at least 4 characters long") - - if next_code() != 256: - raise ValueError("strip must begin with CLEAR code") - - code = 0 - oldcode = 0 - result = [] - result_append = result.append - while True: - code = next_code() # ~5% faster when inlining this function - bitcount += bitw - if code == 257 or bitcount >= bitcount_max: # EOI - break - if code == 256: # CLEAR - table = newtable[:] - table_append = table.append - lentable = 258 - bitw, shr, mask = switchbitch[255] - code = next_code() - bitcount += bitw - if code == 257: # EOI - break - result_append(table[code]) - else: - if code < lentable: - decoded = table[code] - newcode = table[oldcode] + decoded[:1] - else: - newcode = table[oldcode] - newcode += newcode[:1] - decoded = newcode - result_append(decoded) - table_append(newcode) - lentable += 1 - oldcode = code - if lentable in switchbitch: - bitw, shr, mask = switchbitch[lentable] - - if code != 257: - warnings.warn("unexpected end of lzw stream (code %i)" % code) - - return b''.join(result) - - -@_replace_by('_tifffile.unpackints') -def unpackints(data, dtype, itemsize, runlen=0): - """Decompress byte string to array of integers of any bit size <= 32. - - Parameters - ---------- - data : byte str - Data to decompress. - dtype : numpy.dtype or str - A numpy boolean or integer type. - itemsize : int - Number of bits per integer. - runlen : int - Number of consecutive integers, after which to start at next byte. - - """ - if itemsize == 1: # bitarray - data = numpy.fromstring(data, '|B') - data = numpy.unpackbits(data) - if runlen % 8: - data = data.reshape(-1, runlen + (8 - runlen % 8)) - data = data[:, :runlen].reshape(-1) - return data.astype(dtype) - - dtype = numpy.dtype(dtype) - if itemsize in (8, 16, 32, 64): - return numpy.fromstring(data, dtype) - if itemsize < 1 or itemsize > 32: - raise ValueError("itemsize out of range: %i" % itemsize) - if dtype.kind not in "biu": - raise ValueError("invalid dtype") - - itembytes = next(i for i in (1, 2, 4, 8) if 8 * i >= itemsize) - if itembytes != dtype.itemsize: - raise ValueError("dtype.itemsize too small") - if runlen == 0: - runlen = len(data) // itembytes - skipbits = runlen*itemsize % 8 - if skipbits: - skipbits = 8 - skipbits - shrbits = itembytes*8 - itemsize - bitmask = int(itemsize*'1'+'0'*shrbits, 2) - dtypestr = '>' + dtype.char # dtype always big endian? - - unpack = struct.unpack - l = runlen * (len(data)*8 // (runlen*itemsize + skipbits)) - result = numpy.empty((l, ), dtype) - bitcount = 0 - for i in range(len(result)): - start = bitcount // 8 - s = data[start:start+itembytes] - try: - code = unpack(dtypestr, s)[0] - except Exception: - code = unpack(dtypestr, s + b'\x00'*(itembytes-len(s)))[0] - code <<= bitcount % 8 - code &= bitmask - result[i] = code >> shrbits - bitcount += itemsize - if (i+1) % runlen == 0: - bitcount += skipbits - return result - - -def unpackrgb(data, dtype='>> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff) - >>> print(unpackrgb(data, '>> print(unpackrgb(data, '>> print(unpackrgb(data, '= bits) - data = numpy.fromstring(data, dtype.byteorder+dt) - result = numpy.empty((data.size, len(bitspersample)), dtype.char) - for i, bps in enumerate(bitspersample): - t = data >> int(numpy.sum(bitspersample[i+1:])) - t &= int('0b'+'1'*bps, 2) - if rescale: - o = ((dtype.itemsize * 8) // bps + 1) * bps - if o > data.dtype.itemsize * 8: - t = t.astype('I') - t *= (2**o - 1) // (2**bps - 1) - t //= 2**(o - (dtype.itemsize * 8)) - result[:, i] = t - return result.reshape(-1) - - -def reorient(image, orientation): - """Return reoriented view of image array. - - Parameters - ---------- - image : numpy array - Non-squeezed output of asarray() functions. - Axes -3 and -2 must be image length and width respectively. - orientation : int or str - One of TIFF_ORIENTATIONS keys or values. - - """ - o = TIFF_ORIENTATIONS.get(orientation, orientation) - if o == 'top_left': - return image - elif o == 'top_right': - return image[..., ::-1, :] - elif o == 'bottom_left': - return image[..., ::-1, :, :] - elif o == 'bottom_right': - return image[..., ::-1, ::-1, :] - elif o == 'left_top': - return numpy.swapaxes(image, -3, -2) - elif o == 'right_top': - return numpy.swapaxes(image, -3, -2)[..., ::-1, :] - elif o == 'left_bottom': - return numpy.swapaxes(image, -3, -2)[..., ::-1, :, :] - elif o == 'right_bottom': - return numpy.swapaxes(image, -3, -2)[..., ::-1, ::-1, :] - - -def squeeze_axes(shape, axes, skip='XY'): - """Return shape and axes with single-dimensional entries removed. - - Remove unused dimensions unless their axes are listed in 'skip'. - - >>> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC') - ((5, 2, 1), 'TYX') - - """ - if len(shape) != len(axes): - raise ValueError("dimensions of axes and shape don't match") - shape, axes = zip(*(i for i in zip(shape, axes) - if i[0] > 1 or i[1] in skip)) - return shape, ''.join(axes) - - -def transpose_axes(data, axes, asaxes='CTZYX'): - """Return data with its axes permuted to match specified axes. - - A view is returned if possible. - - >>> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape - (5, 2, 1, 3, 4) - - """ - for ax in axes: - if ax not in asaxes: - raise ValueError("unknown axis %s" % ax) - # add missing axes to data - shape = data.shape - for ax in reversed(asaxes): - if ax not in axes: - axes = ax + axes - shape = (1,) + shape - data = data.reshape(shape) - # transpose axes - data = data.transpose([axes.index(ax) for ax in asaxes]) - return data - - -def stack_pages(pages, memmap=False, *args, **kwargs): - """Read data from sequence of TiffPage and stack them vertically. - - If memmap is True, return an array stored in a binary file on disk. - Additional parameters are passsed to the page asarray function. - - """ - if len(pages) == 0: - raise ValueError("no pages") - - if len(pages) == 1: - return pages[0].asarray(memmap=memmap, *args, **kwargs) - - result = pages[0].asarray(*args, **kwargs) - shape = (len(pages),) + result.shape - if memmap: - with tempfile.NamedTemporaryFile() as fh: - result = numpy.memmap(fh, dtype=result.dtype, shape=shape) - else: - result = numpy.empty(shape, dtype=result.dtype) - - for i, page in enumerate(pages): - result[i] = page.asarray(*args, **kwargs) - - return result - - -def stripnull(string): - """Return string truncated at first null character. - - Clean NULL terminated C strings. - - >>> stripnull(b'string\\x00') - b'string' - - """ - i = string.find(b'\x00') - return string if (i < 0) else string[:i] - - -def stripascii(string): - """Return string truncated at last byte that is 7bit ASCII. - - Clean NULL separated and terminated TIFF strings. - - >>> stripascii(b'string\\x00string\\n\\x01\\x00') - b'string\\x00string\\n' - >>> stripascii(b'\\x00') - b'' - - """ - # TODO: pythonize this - ord_ = ord if sys.version_info[0] < 3 else lambda x: x - i = len(string) - while i: - i -= 1 - if 8 < ord_(string[i]) < 127: - break - else: - i = -1 - return string[:i+1] - - -def format_size(size): - """Return file size as string from byte size.""" - for unit in ('B', 'KB', 'MB', 'GB', 'TB'): - if size < 2048: - return "%.f %s" % (size, unit) - size /= 1024.0 - - -def sequence(value): - """Return tuple containing value if value is not a sequence. - - >>> sequence(1) - (1,) - >>> sequence([1]) - [1] - - """ - try: - len(value) - return value - except TypeError: - return (value, ) - - -def product(iterable): - """Return product of sequence of numbers. - - Equivalent of functools.reduce(operator.mul, iterable, 1). - - >>> product([2**8, 2**30]) - 274877906944 - >>> product([]) - 1 - - """ - prod = 1 - for i in iterable: - prod *= i - return prod - - -def natural_sorted(iterable): - """Return human sorted list of strings. - - E.g. for sorting file names. - - >>> natural_sorted(['f1', 'f2', 'f10']) - ['f1', 'f2', 'f10'] - - """ - def sortkey(x): - return [(int(c) if c.isdigit() else c) for c in re.split(numbers, x)] - numbers = re.compile(r'(\d+)') - return sorted(iterable, key=sortkey) - - -def excel_datetime(timestamp, epoch=datetime.datetime.fromordinal(693594)): - """Return datetime object from timestamp in Excel serial format. - - Convert LSM time stamps. - - >>> excel_datetime(40237.029999999795) - datetime.datetime(2010, 2, 28, 0, 43, 11, 999982) - - """ - return epoch + datetime.timedelta(timestamp) - - -def julian_datetime(julianday, milisecond=0): - """Return datetime from days since 1/1/4713 BC and ms since midnight. - - Convert Julian dates according to MetaMorph. - - >>> julian_datetime(2451576, 54362783) - datetime.datetime(2000, 2, 2, 15, 6, 2, 783) - - """ - if julianday <= 1721423: - # no datetime before year 1 - return None - - a = julianday + 1 - if a > 2299160: - alpha = math.trunc((a - 1867216.25) / 36524.25) - a += 1 + alpha - alpha // 4 - b = a + (1524 if a > 1721423 else 1158) - c = math.trunc((b - 122.1) / 365.25) - d = math.trunc(365.25 * c) - e = math.trunc((b - d) / 30.6001) - - day = b - d - math.trunc(30.6001 * e) - month = e - (1 if e < 13.5 else 13) - year = c - (4716 if month > 2.5 else 4715) - - hour, milisecond = divmod(milisecond, 1000 * 60 * 60) - minute, milisecond = divmod(milisecond, 1000 * 60) - second, milisecond = divmod(milisecond, 1000) - - return datetime.datetime(year, month, day, - hour, minute, second, milisecond) - - -def test_tifffile(directory='testimages', verbose=True): - """Read all images in directory. - - Print error message on failure. - - >>> test_tifffile(verbose=False) - - """ - successful = 0 - failed = 0 - start = time.time() - for f in glob.glob(os.path.join(directory, '*.*')): - if verbose: - print("\n%s>\n" % f.lower(), end='') - t0 = time.time() - try: - tif = TiffFile(f, multifile=True) - except Exception as e: - if not verbose: - print(f, end=' ') - print("ERROR:", e) - failed += 1 - continue - try: - img = tif.asarray() - except ValueError: - try: - img = tif[0].asarray() - except Exception as e: - if not verbose: - print(f, end=' ') - print("ERROR:", e) - failed += 1 - continue - finally: - tif.close() - successful += 1 - if verbose: - print("%s, %s %s, %s, %.0f ms" % ( - str(tif), str(img.shape), img.dtype, tif[0].compression, - (time.time()-t0) * 1e3)) - if verbose: - print("\nSuccessfully read %i of %i files in %.3f s\n" % ( - successful, successful+failed, time.time()-start)) - - -class TIFF_SUBFILE_TYPES(object): - def __getitem__(self, key): - result = [] - if key & 1: - result.append('reduced_image') - if key & 2: - result.append('page') - if key & 4: - result.append('mask') - return tuple(result) - - -TIFF_PHOTOMETRICS = { - 0: 'miniswhite', - 1: 'minisblack', - 2: 'rgb', - 3: 'palette', - 4: 'mask', - 5: 'separated', # CMYK - 6: 'ycbcr', - 8: 'cielab', - 9: 'icclab', - 10: 'itulab', - 32803: 'cfa', # Color Filter Array - 32844: 'logl', - 32845: 'logluv', - 34892: 'linear_raw' -} - -TIFF_COMPESSIONS = { - 1: None, - 2: 'ccittrle', - 3: 'ccittfax3', - 4: 'ccittfax4', - 5: 'lzw', - 6: 'ojpeg', - 7: 'jpeg', - 8: 'adobe_deflate', - 9: 't85', - 10: 't43', - 32766: 'next', - 32771: 'ccittrlew', - 32773: 'packbits', - 32809: 'thunderscan', - 32895: 'it8ctpad', - 32896: 'it8lw', - 32897: 'it8mp', - 32898: 'it8bl', - 32908: 'pixarfilm', - 32909: 'pixarlog', - 32946: 'deflate', - 32947: 'dcs', - 34661: 'jbig', - 34676: 'sgilog', - 34677: 'sgilog24', - 34712: 'jp2000', - 34713: 'nef', -} - -TIFF_DECOMPESSORS = { - None: lambda x: x, - 'adobe_deflate': zlib.decompress, - 'deflate': zlib.decompress, - 'packbits': decodepackbits, - 'lzw': decodelzw, - # 'jpeg': decodejpg -} - -TIFF_DATA_TYPES = { - 1: '1B', # BYTE 8-bit unsigned integer. - 2: '1s', # ASCII 8-bit byte that contains a 7-bit ASCII code; - # the last byte must be NULL (binary zero). - 3: '1H', # SHORT 16-bit (2-byte) unsigned integer - 4: '1I', # LONG 32-bit (4-byte) unsigned integer. - 5: '2I', # RATIONAL Two LONGs: the first represents the numerator of - # a fraction; the second, the denominator. - 6: '1b', # SBYTE An 8-bit signed (twos-complement) integer. - 7: '1s', # UNDEFINED An 8-bit byte that may contain anything, - # depending on the definition of the field. - 8: '1h', # SSHORT A 16-bit (2-byte) signed (twos-complement) integer. - 9: '1i', # SLONG A 32-bit (4-byte) signed (twos-complement) integer. - 10: '2i', # SRATIONAL Two SLONGs: the first represents the numerator - # of a fraction, the second the denominator. - 11: '1f', # FLOAT Single precision (4-byte) IEEE format. - 12: '1d', # DOUBLE Double precision (8-byte) IEEE format. - 13: '1I', # IFD unsigned 4 byte IFD offset. - #14: '', # UNICODE - #15: '', # COMPLEX - 16: '1Q', # LONG8 unsigned 8 byte integer (BigTiff) - 17: '1q', # SLONG8 signed 8 byte integer (BigTiff) - 18: '1Q', # IFD8 unsigned 8 byte IFD offset (BigTiff) -} - -TIFF_SAMPLE_FORMATS = { - 1: 'uint', - 2: 'int', - 3: 'float', - #4: 'void', - #5: 'complex_int', - 6: 'complex', -} - -TIFF_SAMPLE_DTYPES = { - ('uint', 1): '?', # bitmap - ('uint', 2): 'B', - ('uint', 3): 'B', - ('uint', 4): 'B', - ('uint', 5): 'B', - ('uint', 6): 'B', - ('uint', 7): 'B', - ('uint', 8): 'B', - ('uint', 9): 'H', - ('uint', 10): 'H', - ('uint', 11): 'H', - ('uint', 12): 'H', - ('uint', 13): 'H', - ('uint', 14): 'H', - ('uint', 15): 'H', - ('uint', 16): 'H', - ('uint', 17): 'I', - ('uint', 18): 'I', - ('uint', 19): 'I', - ('uint', 20): 'I', - ('uint', 21): 'I', - ('uint', 22): 'I', - ('uint', 23): 'I', - ('uint', 24): 'I', - ('uint', 25): 'I', - ('uint', 26): 'I', - ('uint', 27): 'I', - ('uint', 28): 'I', - ('uint', 29): 'I', - ('uint', 30): 'I', - ('uint', 31): 'I', - ('uint', 32): 'I', - ('uint', 64): 'Q', - ('int', 8): 'b', - ('int', 16): 'h', - ('int', 32): 'i', - ('int', 64): 'q', - ('float', 16): 'e', - ('float', 32): 'f', - ('float', 64): 'd', - ('complex', 64): 'F', - ('complex', 128): 'D', - ('uint', (5, 6, 5)): 'B', -} - -TIFF_ORIENTATIONS = { - 1: 'top_left', - 2: 'top_right', - 3: 'bottom_right', - 4: 'bottom_left', - 5: 'left_top', - 6: 'right_top', - 7: 'right_bottom', - 8: 'left_bottom', -} - -# TODO: is there a standard for character axes labels? -AXES_LABELS = { - 'X': 'width', - 'Y': 'height', - 'Z': 'depth', - 'S': 'sample', # rgb(a) - 'I': 'series', # general sequence, plane, page, IFD - 'T': 'time', - 'C': 'channel', # color, emission wavelength - 'A': 'angle', - 'P': 'phase', # formerly F # P is Position in LSM! - 'R': 'tile', # region, point, mosaic - 'H': 'lifetime', # histogram - 'E': 'lambda', # excitation wavelength - 'L': 'exposure', # lux - 'V': 'event', - 'Q': 'other', - #'M': 'mosaic', # LSM 6 -} - -AXES_LABELS.update(dict((v, k) for k, v in AXES_LABELS.items())) - -# Map OME pixel types to numpy dtype -OME_PIXEL_TYPES = { - 'int8': 'i1', - 'int16': 'i2', - 'int32': 'i4', - 'uint8': 'u1', - 'uint16': 'u2', - 'uint32': 'u4', - 'float': 'f4', - # 'bit': 'bit', - 'double': 'f8', - 'complex': 'c8', - 'double-complex': 'c16', -} - -# NIH Image PicHeader v1.63 -NIH_IMAGE_HEADER = [ - ('fileid', 'a8'), - ('nlines', 'i2'), - ('pixelsperline', 'i2'), - ('version', 'i2'), - ('oldlutmode', 'i2'), - ('oldncolors', 'i2'), - ('colors', 'u1', (3, 32)), - ('oldcolorstart', 'i2'), - ('colorwidth', 'i2'), - ('extracolors', 'u2', (6, 3)), - ('nextracolors', 'i2'), - ('foregroundindex', 'i2'), - ('backgroundindex', 'i2'), - ('xscale', 'f8'), - ('_x0', 'i2'), - ('_x1', 'i2'), - ('units_t', 'i2'), # NIH_UNITS_TYPE - ('p1', [('x', 'i2'), ('y', 'i2')]), - ('p2', [('x', 'i2'), ('y', 'i2')]), - ('curvefit_t', 'i2'), # NIH_CURVEFIT_TYPE - ('ncoefficients', 'i2'), - ('coeff', 'f8', 6), - ('_um_len', 'u1'), - ('um', 'a15'), - ('_x2', 'u1'), - ('binarypic', 'b1'), - ('slicestart', 'i2'), - ('sliceend', 'i2'), - ('scalemagnification', 'f4'), - ('nslices', 'i2'), - ('slicespacing', 'f4'), - ('currentslice', 'i2'), - ('frameinterval', 'f4'), - ('pixelaspectratio', 'f4'), - ('colorstart', 'i2'), - ('colorend', 'i2'), - ('ncolors', 'i2'), - ('fill1', '3u2'), - ('fill2', '3u2'), - ('colortable_t', 'u1'), # NIH_COLORTABLE_TYPE - ('lutmode_t', 'u1'), # NIH_LUTMODE_TYPE - ('invertedtable', 'b1'), - ('zeroclip', 'b1'), - ('_xunit_len', 'u1'), - ('xunit', 'a11'), - ('stacktype_t', 'i2'), # NIH_STACKTYPE_TYPE -] - -NIH_COLORTABLE_TYPE = ( - 'CustomTable', 'AppleDefault', 'Pseudo20', 'Pseudo32', 'Rainbow', - 'Fire1', 'Fire2', 'Ice', 'Grays', 'Spectrum') - -NIH_LUTMODE_TYPE = ( - 'PseudoColor', 'OldAppleDefault', 'OldSpectrum', 'GrayScale', - 'ColorLut', 'CustomGrayscale') - -NIH_CURVEFIT_TYPE = ( - 'StraightLine', 'Poly2', 'Poly3', 'Poly4', 'Poly5', 'ExpoFit', - 'PowerFit', 'LogFit', 'RodbardFit', 'SpareFit1', 'Uncalibrated', - 'UncalibratedOD') - -NIH_UNITS_TYPE = ( - 'Nanometers', 'Micrometers', 'Millimeters', 'Centimeters', 'Meters', - 'Kilometers', 'Inches', 'Feet', 'Miles', 'Pixels', 'OtherUnits') - -NIH_STACKTYPE_TYPE = ( - 'VolumeStack', 'RGBStack', 'MovieStack', 'HSVStack') - -# Map Universal Imaging Corporation MetaMorph internal tag ids to name and type -UIC_TAGS = { - 0: ('auto_scale', int), - 1: ('min_scale', int), - 2: ('max_scale', int), - 3: ('spatial_calibration', int), - 4: ('x_calibration', Fraction), - 5: ('y_calibration', Fraction), - 6: ('calibration_units', str), - 7: ('name', str), - 8: ('thresh_state', int), - 9: ('thresh_state_red', int), - 10: ('tagid_10', None), # undefined - 11: ('thresh_state_green', int), - 12: ('thresh_state_blue', int), - 13: ('thresh_state_lo', int), - 14: ('thresh_state_hi', int), - 15: ('zoom', int), - 16: ('create_time', julian_datetime), - 17: ('last_saved_time', julian_datetime), - 18: ('current_buffer', int), - 19: ('gray_fit', None), - 20: ('gray_point_count', None), - 21: ('gray_x', Fraction), - 22: ('gray_y', Fraction), - 23: ('gray_min', Fraction), - 24: ('gray_max', Fraction), - 25: ('gray_unit_name', str), - 26: ('standard_lut', int), - 27: ('wavelength', int), - 28: ('stage_position', '(%i,2,2)u4'), # N xy positions as fractions - 29: ('camera_chip_offset', '(%i,2,2)u4'), # N xy offsets as fractions - 30: ('overlay_mask', None), - 31: ('overlay_compress', None), - 32: ('overlay', None), - 33: ('special_overlay_mask', None), - 34: ('special_overlay_compress', None), - 35: ('special_overlay', None), - 36: ('image_property', read_uic_image_property), - 37: ('stage_label', '%ip'), # N str - 38: ('autoscale_lo_info', Fraction), - 39: ('autoscale_hi_info', Fraction), - 40: ('absolute_z', '(%i,2)u4'), # N fractions - 41: ('absolute_z_valid', '(%i,)u4'), # N long - 42: ('gamma', int), - 43: ('gamma_red', int), - 44: ('gamma_green', int), - 45: ('gamma_blue', int), - 46: ('camera_bin', int), - 47: ('new_lut', int), - 48: ('image_property_ex', None), - 49: ('plane_property', int), - 50: ('user_lut_table', '(256,3)u1'), - 51: ('red_autoscale_info', int), - 52: ('red_autoscale_lo_info', Fraction), - 53: ('red_autoscale_hi_info', Fraction), - 54: ('red_minscale_info', int), - 55: ('red_maxscale_info', int), - 56: ('green_autoscale_info', int), - 57: ('green_autoscale_lo_info', Fraction), - 58: ('green_autoscale_hi_info', Fraction), - 59: ('green_minscale_info', int), - 60: ('green_maxscale_info', int), - 61: ('blue_autoscale_info', int), - 62: ('blue_autoscale_lo_info', Fraction), - 63: ('blue_autoscale_hi_info', Fraction), - 64: ('blue_min_scale_info', int), - 65: ('blue_max_scale_info', int), - #66: ('overlay_plane_color', read_uic_overlay_plane_color), -} - - -# Olympus FluoView -MM_DIMENSION = [ - ('name', 'a16'), - ('size', 'i4'), - ('origin', 'f8'), - ('resolution', 'f8'), - ('unit', 'a64'), -] - -MM_HEADER = [ - ('header_flag', 'i2'), - ('image_type', 'u1'), - ('image_name', 'a257'), - ('offset_data', 'u4'), - ('palette_size', 'i4'), - ('offset_palette0', 'u4'), - ('offset_palette1', 'u4'), - ('comment_size', 'i4'), - ('offset_comment', 'u4'), - ('dimensions', MM_DIMENSION, 10), - ('offset_position', 'u4'), - ('map_type', 'i2'), - ('map_min', 'f8'), - ('map_max', 'f8'), - ('min_value', 'f8'), - ('max_value', 'f8'), - ('offset_map', 'u4'), - ('gamma', 'f8'), - ('offset', 'f8'), - ('gray_channel', MM_DIMENSION), - ('offset_thumbnail', 'u4'), - ('voice_field', 'i4'), - ('offset_voice_field', 'u4'), -] - -# Carl Zeiss LSM -CZ_LSM_INFO = [ - ('magic_number', 'u4'), - ('structure_size', 'i4'), - ('dimension_x', 'i4'), - ('dimension_y', 'i4'), - ('dimension_z', 'i4'), - ('dimension_channels', 'i4'), - ('dimension_time', 'i4'), - ('data_type', 'i4'), # CZ_DATA_TYPES - ('thumbnail_x', 'i4'), - ('thumbnail_y', 'i4'), - ('voxel_size_x', 'f8'), - ('voxel_size_y', 'f8'), - ('voxel_size_z', 'f8'), - ('origin_x', 'f8'), - ('origin_y', 'f8'), - ('origin_z', 'f8'), - ('scan_type', 'u2'), - ('spectral_scan', 'u2'), - ('type_of_data', 'u4'), # CZ_TYPE_OF_DATA - ('offset_vector_overlay', 'u4'), - ('offset_input_lut', 'u4'), - ('offset_output_lut', 'u4'), - ('offset_channel_colors', 'u4'), - ('time_interval', 'f8'), - ('offset_channel_data_types', 'u4'), - ('offset_scan_info', 'u4'), # CZ_LSM_SCAN_INFO - ('offset_ks_data', 'u4'), - ('offset_time_stamps', 'u4'), - ('offset_event_list', 'u4'), - ('offset_roi', 'u4'), - ('offset_bleach_roi', 'u4'), - ('offset_next_recording', 'u4'), - # LSM 2.0 ends here - ('display_aspect_x', 'f8'), - ('display_aspect_y', 'f8'), - ('display_aspect_z', 'f8'), - ('display_aspect_time', 'f8'), - ('offset_mean_of_roi_overlay', 'u4'), - ('offset_topo_isoline_overlay', 'u4'), - ('offset_topo_profile_overlay', 'u4'), - ('offset_linescan_overlay', 'u4'), - ('offset_toolbar_flags', 'u4'), - ('offset_channel_wavelength', 'u4'), - ('offset_channel_factors', 'u4'), - ('objective_sphere_correction', 'f8'), - ('offset_unmix_parameters', 'u4'), - # LSM 3.2, 4.0 end here - ('offset_acquisition_parameters', 'u4'), - ('offset_characteristics', 'u4'), - ('offset_palette', 'u4'), - ('time_difference_x', 'f8'), - ('time_difference_y', 'f8'), - ('time_difference_z', 'f8'), - ('internal_use_1', 'u4'), - ('dimension_p', 'i4'), - ('dimension_m', 'i4'), - ('dimensions_reserved', '16i4'), - ('offset_tile_positions', 'u4'), - ('reserved_1', '9u4'), - ('offset_positions', 'u4'), - ('reserved_2', '21u4'), # must be 0 -] - -# Import functions for LSM_INFO sub-records -CZ_LSM_INFO_READERS = { - 'scan_info': read_cz_lsm_scan_info, - 'time_stamps': read_cz_lsm_time_stamps, - 'event_list': read_cz_lsm_event_list, - 'channel_colors': read_cz_lsm_floatpairs, - 'positions': read_cz_lsm_floatpairs, - 'tile_positions': read_cz_lsm_floatpairs, -} - -# Map cz_lsm_info.scan_type to dimension order -CZ_SCAN_TYPES = { - 0: 'XYZCT', # x-y-z scan - 1: 'XYZCT', # z scan (x-z plane) - 2: 'XYZCT', # line scan - 3: 'XYTCZ', # time series x-y - 4: 'XYZTC', # time series x-z - 5: 'XYTCZ', # time series 'Mean of ROIs' - 6: 'XYZTC', # time series x-y-z - 7: 'XYCTZ', # spline scan - 8: 'XYCZT', # spline scan x-z - 9: 'XYTCZ', # time series spline plane x-z - 10: 'XYZCT', # point mode -} - -# Map dimension codes to cz_lsm_info attribute -CZ_DIMENSIONS = { - 'X': 'dimension_x', - 'Y': 'dimension_y', - 'Z': 'dimension_z', - 'C': 'dimension_channels', - 'T': 'dimension_time', -} - -# Description of cz_lsm_info.data_type -CZ_DATA_TYPES = { - 0: 'varying data types', - 1: '8 bit unsigned integer', - 2: '12 bit unsigned integer', - 5: '32 bit float', -} - -# Description of cz_lsm_info.type_of_data -CZ_TYPE_OF_DATA = { - 0: 'Original scan data', - 1: 'Calculated data', - 2: '3D reconstruction', - 3: 'Topography height map', -} - -CZ_LSM_SCAN_INFO_ARRAYS = { - 0x20000000: "tracks", - 0x30000000: "lasers", - 0x60000000: "detection_channels", - 0x80000000: "illumination_channels", - 0xa0000000: "beam_splitters", - 0xc0000000: "data_channels", - 0x11000000: "timers", - 0x13000000: "markers", -} - -CZ_LSM_SCAN_INFO_STRUCTS = { - # 0x10000000: "recording", - 0x40000000: "track", - 0x50000000: "laser", - 0x70000000: "detection_channel", - 0x90000000: "illumination_channel", - 0xb0000000: "beam_splitter", - 0xd0000000: "data_channel", - 0x12000000: "timer", - 0x14000000: "marker", -} - -CZ_LSM_SCAN_INFO_ATTRIBUTES = { - # recording - 0x10000001: "name", - 0x10000002: "description", - 0x10000003: "notes", - 0x10000004: "objective", - 0x10000005: "processing_summary", - 0x10000006: "special_scan_mode", - 0x10000007: "scan_type", - 0x10000008: "scan_mode", - 0x10000009: "number_of_stacks", - 0x1000000a: "lines_per_plane", - 0x1000000b: "samples_per_line", - 0x1000000c: "planes_per_volume", - 0x1000000d: "images_width", - 0x1000000e: "images_height", - 0x1000000f: "images_number_planes", - 0x10000010: "images_number_stacks", - 0x10000011: "images_number_channels", - 0x10000012: "linscan_xy_size", - 0x10000013: "scan_direction", - 0x10000014: "time_series", - 0x10000015: "original_scan_data", - 0x10000016: "zoom_x", - 0x10000017: "zoom_y", - 0x10000018: "zoom_z", - 0x10000019: "sample_0x", - 0x1000001a: "sample_0y", - 0x1000001b: "sample_0z", - 0x1000001c: "sample_spacing", - 0x1000001d: "line_spacing", - 0x1000001e: "plane_spacing", - 0x1000001f: "plane_width", - 0x10000020: "plane_height", - 0x10000021: "volume_depth", - 0x10000023: "nutation", - 0x10000034: "rotation", - 0x10000035: "precession", - 0x10000036: "sample_0time", - 0x10000037: "start_scan_trigger_in", - 0x10000038: "start_scan_trigger_out", - 0x10000039: "start_scan_event", - 0x10000040: "start_scan_time", - 0x10000041: "stop_scan_trigger_in", - 0x10000042: "stop_scan_trigger_out", - 0x10000043: "stop_scan_event", - 0x10000044: "stop_scan_time", - 0x10000045: "use_rois", - 0x10000046: "use_reduced_memory_rois", - 0x10000047: "user", - 0x10000048: "use_bc_correction", - 0x10000049: "position_bc_correction1", - 0x10000050: "position_bc_correction2", - 0x10000051: "interpolation_y", - 0x10000052: "camera_binning", - 0x10000053: "camera_supersampling", - 0x10000054: "camera_frame_width", - 0x10000055: "camera_frame_height", - 0x10000056: "camera_offset_x", - 0x10000057: "camera_offset_y", - 0x10000059: "rt_binning", - 0x1000005a: "rt_frame_width", - 0x1000005b: "rt_frame_height", - 0x1000005c: "rt_region_width", - 0x1000005d: "rt_region_height", - 0x1000005e: "rt_offset_x", - 0x1000005f: "rt_offset_y", - 0x10000060: "rt_zoom", - 0x10000061: "rt_line_period", - 0x10000062: "prescan", - 0x10000063: "scan_direction_z", - # track - 0x40000001: "multiplex_type", # 0 after line; 1 after frame - 0x40000002: "multiplex_order", - 0x40000003: "sampling_mode", # 0 sample; 1 line average; 2 frame average - 0x40000004: "sampling_method", # 1 mean; 2 sum - 0x40000005: "sampling_number", - 0x40000006: "acquire", - 0x40000007: "sample_observation_time", - 0x4000000b: "time_between_stacks", - 0x4000000c: "name", - 0x4000000d: "collimator1_name", - 0x4000000e: "collimator1_position", - 0x4000000f: "collimator2_name", - 0x40000010: "collimator2_position", - 0x40000011: "is_bleach_track", - 0x40000012: "is_bleach_after_scan_number", - 0x40000013: "bleach_scan_number", - 0x40000014: "trigger_in", - 0x40000015: "trigger_out", - 0x40000016: "is_ratio_track", - 0x40000017: "bleach_count", - 0x40000018: "spi_center_wavelength", - 0x40000019: "pixel_time", - 0x40000021: "condensor_frontlens", - 0x40000023: "field_stop_value", - 0x40000024: "id_condensor_aperture", - 0x40000025: "condensor_aperture", - 0x40000026: "id_condensor_revolver", - 0x40000027: "condensor_filter", - 0x40000028: "id_transmission_filter1", - 0x40000029: "id_transmission1", - 0x40000030: "id_transmission_filter2", - 0x40000031: "id_transmission2", - 0x40000032: "repeat_bleach", - 0x40000033: "enable_spot_bleach_pos", - 0x40000034: "spot_bleach_posx", - 0x40000035: "spot_bleach_posy", - 0x40000036: "spot_bleach_posz", - 0x40000037: "id_tubelens", - 0x40000038: "id_tubelens_position", - 0x40000039: "transmitted_light", - 0x4000003a: "reflected_light", - 0x4000003b: "simultan_grab_and_bleach", - 0x4000003c: "bleach_pixel_time", - # laser - 0x50000001: "name", - 0x50000002: "acquire", - 0x50000003: "power", - # detection_channel - 0x70000001: "integration_mode", - 0x70000002: "special_mode", - 0x70000003: "detector_gain_first", - 0x70000004: "detector_gain_last", - 0x70000005: "amplifier_gain_first", - 0x70000006: "amplifier_gain_last", - 0x70000007: "amplifier_offs_first", - 0x70000008: "amplifier_offs_last", - 0x70000009: "pinhole_diameter", - 0x7000000a: "counting_trigger", - 0x7000000b: "acquire", - 0x7000000c: "point_detector_name", - 0x7000000d: "amplifier_name", - 0x7000000e: "pinhole_name", - 0x7000000f: "filter_set_name", - 0x70000010: "filter_name", - 0x70000013: "integrator_name", - 0x70000014: "channel_name", - 0x70000015: "detector_gain_bc1", - 0x70000016: "detector_gain_bc2", - 0x70000017: "amplifier_gain_bc1", - 0x70000018: "amplifier_gain_bc2", - 0x70000019: "amplifier_offset_bc1", - 0x70000020: "amplifier_offset_bc2", - 0x70000021: "spectral_scan_channels", - 0x70000022: "spi_wavelength_start", - 0x70000023: "spi_wavelength_stop", - 0x70000026: "dye_name", - 0x70000027: "dye_folder", - # illumination_channel - 0x90000001: "name", - 0x90000002: "power", - 0x90000003: "wavelength", - 0x90000004: "aquire", - 0x90000005: "detchannel_name", - 0x90000006: "power_bc1", - 0x90000007: "power_bc2", - # beam_splitter - 0xb0000001: "filter_set", - 0xb0000002: "filter", - 0xb0000003: "name", - # data_channel - 0xd0000001: "name", - 0xd0000003: "acquire", - 0xd0000004: "color", - 0xd0000005: "sample_type", - 0xd0000006: "bits_per_sample", - 0xd0000007: "ratio_type", - 0xd0000008: "ratio_track1", - 0xd0000009: "ratio_track2", - 0xd000000a: "ratio_channel1", - 0xd000000b: "ratio_channel2", - 0xd000000c: "ratio_const1", - 0xd000000d: "ratio_const2", - 0xd000000e: "ratio_const3", - 0xd000000f: "ratio_const4", - 0xd0000010: "ratio_const5", - 0xd0000011: "ratio_const6", - 0xd0000012: "ratio_first_images1", - 0xd0000013: "ratio_first_images2", - 0xd0000014: "dye_name", - 0xd0000015: "dye_folder", - 0xd0000016: "spectrum", - 0xd0000017: "acquire", - # timer - 0x12000001: "name", - 0x12000002: "description", - 0x12000003: "interval", - 0x12000004: "trigger_in", - 0x12000005: "trigger_out", - 0x12000006: "activation_time", - 0x12000007: "activation_number", - # marker - 0x14000001: "name", - 0x14000002: "description", - 0x14000003: "trigger_in", - 0x14000004: "trigger_out", -} - -# Map TIFF tag code to attribute name, default value, type, count, validator -TIFF_TAGS = { - 254: ('new_subfile_type', 0, 4, 1, TIFF_SUBFILE_TYPES()), - 255: ('subfile_type', None, 3, 1, - {0: 'undefined', 1: 'image', 2: 'reduced_image', 3: 'page'}), - 256: ('image_width', None, 4, 1, None), - 257: ('image_length', None, 4, 1, None), - 258: ('bits_per_sample', 1, 3, 1, None), - 259: ('compression', 1, 3, 1, TIFF_COMPESSIONS), - 262: ('photometric', None, 3, 1, TIFF_PHOTOMETRICS), - 266: ('fill_order', 1, 3, 1, {1: 'msb2lsb', 2: 'lsb2msb'}), - 269: ('document_name', None, 2, None, None), - 270: ('image_description', None, 2, None, None), - 271: ('make', None, 2, None, None), - 272: ('model', None, 2, None, None), - 273: ('strip_offsets', None, 4, None, None), - 274: ('orientation', 1, 3, 1, TIFF_ORIENTATIONS), - 277: ('samples_per_pixel', 1, 3, 1, None), - 278: ('rows_per_strip', 2**32-1, 4, 1, None), - 279: ('strip_byte_counts', None, 4, None, None), - 280: ('min_sample_value', None, 3, None, None), - 281: ('max_sample_value', None, 3, None, None), # 2**bits_per_sample - 282: ('x_resolution', None, 5, 1, None), - 283: ('y_resolution', None, 5, 1, None), - 284: ('planar_configuration', 1, 3, 1, {1: 'contig', 2: 'separate'}), - 285: ('page_name', None, 2, None, None), - 286: ('x_position', None, 5, 1, None), - 287: ('y_position', None, 5, 1, None), - 296: ('resolution_unit', 2, 4, 1, {1: 'none', 2: 'inch', 3: 'centimeter'}), - 297: ('page_number', None, 3, 2, None), - 305: ('software', None, 2, None, None), - 306: ('datetime', None, 2, None, None), - 315: ('artist', None, 2, None, None), - 316: ('host_computer', None, 2, None, None), - 317: ('predictor', 1, 3, 1, {1: None, 2: 'horizontal'}), - 318: ('white_point', None, 5, 2, None), - 319: ('primary_chromaticities', None, 5, 6, None), - 320: ('color_map', None, 3, None, None), - 322: ('tile_width', None, 4, 1, None), - 323: ('tile_length', None, 4, 1, None), - 324: ('tile_offsets', None, 4, None, None), - 325: ('tile_byte_counts', None, 4, None, None), - 338: ('extra_samples', None, 3, None, - {0: 'unspecified', 1: 'assocalpha', 2: 'unassalpha'}), - 339: ('sample_format', 1, 3, 1, TIFF_SAMPLE_FORMATS), - 340: ('smin_sample_value', None, None, None, None), - 341: ('smax_sample_value', None, None, None, None), - 347: ('jpeg_tables', None, 7, None, None), - 530: ('ycbcr_subsampling', 1, 3, 2, None), - 531: ('ycbcr_positioning', 1, 3, 1, None), - 32996: ('sgi_matteing', None, None, 1, None), # use extra_samples - 32996: ('sgi_datatype', None, None, 1, None), # use sample_format - 32997: ('image_depth', None, 4, 1, None), - 32998: ('tile_depth', None, 4, 1, None), - 33432: ('copyright', None, 1, None, None), - 33445: ('md_file_tag', None, 4, 1, None), - 33446: ('md_scale_pixel', None, 5, 1, None), - 33447: ('md_color_table', None, 3, None, None), - 33448: ('md_lab_name', None, 2, None, None), - 33449: ('md_sample_info', None, 2, None, None), - 33450: ('md_prep_date', None, 2, None, None), - 33451: ('md_prep_time', None, 2, None, None), - 33452: ('md_file_units', None, 2, None, None), - 33550: ('model_pixel_scale', None, 12, 3, None), - 33922: ('model_tie_point', None, 12, None, None), - 34665: ('exif_ifd', None, None, 1, None), - 34735: ('geo_key_directory', None, 3, None, None), - 34736: ('geo_double_params', None, 12, None, None), - 34737: ('geo_ascii_params', None, 2, None, None), - 34853: ('gps_ifd', None, None, 1, None), - 37510: ('user_comment', None, None, None, None), - 42112: ('gdal_metadata', None, 2, None, None), - 42113: ('gdal_nodata', None, 2, None, None), - 50289: ('mc_xy_position', None, 12, 2, None), - 50290: ('mc_z_position', None, 12, 1, None), - 50291: ('mc_xy_calibration', None, 12, 3, None), - 50292: ('mc_lens_lem_na_n', None, 12, 3, None), - 50293: ('mc_channel_name', None, 1, None, None), - 50294: ('mc_ex_wavelength', None, 12, 1, None), - 50295: ('mc_time_stamp', None, 12, 1, None), - 50838: ('imagej_byte_counts', None, None, None, None), - 65200: ('flex_xml', None, 2, None, None), - # code: (attribute name, default value, type, count, validator) -} - -# Map custom TIFF tag codes to attribute names and import functions -CUSTOM_TAGS = { - 700: ('xmp', read_bytes), - 34377: ('photoshop', read_numpy), - 33723: ('iptc', read_bytes), - 34675: ('icc_profile', read_bytes), - 33628: ('uic1tag', read_uic1tag), # Universal Imaging Corporation STK - 33629: ('uic2tag', read_uic2tag), - 33630: ('uic3tag', read_uic3tag), - 33631: ('uic4tag', read_uic4tag), - 34361: ('mm_header', read_mm_header), # Olympus FluoView - 34362: ('mm_stamp', read_mm_stamp), - 34386: ('mm_user_block', read_bytes), - 34412: ('cz_lsm_info', read_cz_lsm_info), # Carl Zeiss LSM - 43314: ('nih_image_header', read_nih_image_header), - # 40001: ('mc_ipwinscal', read_bytes), - 40100: ('mc_id_old', read_bytes), - 50288: ('mc_id', read_bytes), - 50296: ('mc_frame_properties', read_bytes), - 50839: ('imagej_metadata', read_bytes), - 51123: ('micromanager_metadata', read_json), -} - -# Max line length of printed output -PRINT_LINE_LEN = 79 - - -def imshow(data, title=None, vmin=0, vmax=None, cmap=None, - bitspersample=None, photometric='rgb', interpolation='nearest', - dpi=96, figure=None, subplot=111, maxdim=8192, **kwargs): - """Plot n-dimensional images using matplotlib.pyplot. - - Return figure, subplot and plot axis. - Requires pyplot already imported ``from matplotlib import pyplot``. - - Parameters - ---------- - bitspersample : int or None - Number of bits per channel in integer RGB images. - photometric : {'miniswhite', 'minisblack', 'rgb', or 'palette'} - The color space of the image data. - title : str - Window and subplot title. - figure : matplotlib.figure.Figure (optional). - Matplotlib to use for plotting. - subplot : int - A matplotlib.pyplot.subplot axis. - maxdim : int - maximum image size in any dimension. - kwargs : optional - Arguments for matplotlib.pyplot.imshow. - - """ - #if photometric not in ('miniswhite', 'minisblack', 'rgb', 'palette'): - # raise ValueError("Can't handle %s photometrics" % photometric) - # TODO: handle photometric == 'separated' (CMYK) - isrgb = photometric in ('rgb', 'palette') - data = numpy.atleast_2d(data.squeeze()) - data = data[(slice(0, maxdim), ) * len(data.shape)] - - dims = data.ndim - if dims < 2: - raise ValueError("not an image") - elif dims == 2: - dims = 0 - isrgb = False - else: - if isrgb and data.shape[-3] in (3, 4): - data = numpy.swapaxes(data, -3, -2) - data = numpy.swapaxes(data, -2, -1) - elif not isrgb and (data.shape[-1] < data.shape[-2] // 16 and - data.shape[-1] < data.shape[-3] // 16 and - data.shape[-1] < 5): - data = numpy.swapaxes(data, -3, -1) - data = numpy.swapaxes(data, -2, -1) - isrgb = isrgb and data.shape[-1] in (3, 4) - dims -= 3 if isrgb else 2 - - if photometric == 'palette' and isrgb: - datamax = data.max() - if datamax > 255: - data >>= 8 # possible precision loss - data = data.astype('B') - elif data.dtype.kind in 'ui': - if not (isrgb and data.dtype.itemsize <= 1) or bitspersample is None: - try: - bitspersample = int(math.ceil(math.log(data.max(), 2))) - except Exception: - bitspersample = data.dtype.itemsize * 8 - elif not isinstance(bitspersample, int): - # bitspersample can be tuple, e.g. (5, 6, 5) - bitspersample = data.dtype.itemsize * 8 - datamax = 2**bitspersample - if isrgb: - if bitspersample < 8: - data <<= 8 - bitspersample - elif bitspersample > 8: - data >>= bitspersample - 8 # precision loss - data = data.astype('B') - elif data.dtype.kind == 'f': - datamax = data.max() - if isrgb and datamax > 1.0: - if data.dtype.char == 'd': - data = data.astype('f') - data /= datamax - elif data.dtype.kind == 'b': - datamax = 1 - elif data.dtype.kind == 'c': - raise NotImplementedError("complex type") # TODO: handle complex types - - if not isrgb: - if vmax is None: - vmax = datamax - if vmin is None: - if data.dtype.kind == 'i': - dtmin = numpy.iinfo(data.dtype).min - vmin = numpy.min(data) - if vmin == dtmin: - vmin = numpy.min(data > dtmin) - if data.dtype.kind == 'f': - dtmin = numpy.finfo(data.dtype).min - vmin = numpy.min(data) - if vmin == dtmin: - vmin = numpy.min(data > dtmin) - else: - vmin = 0 - - pyplot = sys.modules['matplotlib.pyplot'] - - if figure is None: - pyplot.rc('font', family='sans-serif', weight='normal', size=8) - figure = pyplot.figure(dpi=dpi, figsize=(10.3, 6.3), frameon=True, - facecolor='1.0', edgecolor='w') - try: - figure.canvas.manager.window.title(title) - except Exception: - pass - pyplot.subplots_adjust(bottom=0.03*(dims+2), top=0.9, - left=0.1, right=0.95, hspace=0.05, wspace=0.0) - subplot = pyplot.subplot(subplot) - - if title: - try: - title = unicode(title, 'Windows-1252') - except TypeError: - pass - pyplot.title(title, size=11) - - if cmap is None: - if data.dtype.kind in 'ubf' or vmin == 0: - cmap = 'cubehelix' - else: - cmap = 'coolwarm' - if photometric == 'miniswhite': - cmap += '_r' - - image = pyplot.imshow(data[(0, ) * dims].squeeze(), vmin=vmin, vmax=vmax, - cmap=cmap, interpolation=interpolation, **kwargs) - - if not isrgb: - pyplot.colorbar() # panchor=(0.55, 0.5), fraction=0.05 - - def format_coord(x, y): - # callback function to format coordinate display in toolbar - x = int(x + 0.5) - y = int(y + 0.5) - try: - if dims: - return "%s @ %s [%4i, %4i]" % (cur_ax_dat[1][y, x], - current, x, y) - else: - return "%s @ [%4i, %4i]" % (data[y, x], x, y) - except IndexError: - return "" - - pyplot.gca().format_coord = format_coord - - if dims: - current = list((0, ) * dims) - cur_ax_dat = [0, data[tuple(current)].squeeze()] - sliders = [pyplot.Slider( - pyplot.axes([0.125, 0.03*(axis+1), 0.725, 0.025]), - 'Dimension %i' % axis, 0, data.shape[axis]-1, 0, facecolor='0.5', - valfmt='%%.0f [%i]' % data.shape[axis]) for axis in range(dims)] - for slider in sliders: - slider.drawon = False - - def set_image(current, sliders=sliders, data=data): - # change image and redraw canvas - cur_ax_dat[1] = data[tuple(current)].squeeze() - image.set_data(cur_ax_dat[1]) - for ctrl, index in zip(sliders, current): - ctrl.eventson = False - ctrl.set_val(index) - ctrl.eventson = True - figure.canvas.draw() - - def on_changed(index, axis, data=data, current=current): - # callback function for slider change event - index = int(round(index)) - cur_ax_dat[0] = axis - if index == current[axis]: - return - if index >= data.shape[axis]: - index = 0 - elif index < 0: - index = data.shape[axis] - 1 - current[axis] = index - set_image(current) - - def on_keypressed(event, data=data, current=current): - # callback function for key press event - key = event.key - axis = cur_ax_dat[0] - if str(key) in '0123456789': - on_changed(key, axis) - elif key == 'right': - on_changed(current[axis] + 1, axis) - elif key == 'left': - on_changed(current[axis] - 1, axis) - elif key == 'up': - cur_ax_dat[0] = 0 if axis == len(data.shape)-1 else axis + 1 - elif key == 'down': - cur_ax_dat[0] = len(data.shape)-1 if axis == 0 else axis - 1 - elif key == 'end': - on_changed(data.shape[axis] - 1, axis) - elif key == 'home': - on_changed(0, axis) - - figure.canvas.mpl_connect('key_press_event', on_keypressed) - for axis, ctrl in enumerate(sliders): - ctrl.on_changed(lambda k, a=axis: on_changed(k, a)) - - return figure, subplot, image - - -def _app_show(): - """Block the GUI. For use as skimage plugin.""" - pyplot = sys.modules['matplotlib.pyplot'] - pyplot.show() - - -def main(argv=None): - """Command line usage main function.""" - if float(sys.version[0:3]) < 2.6: - print("This script requires Python version 2.6 or better.") - print("This is Python version %s" % sys.version) - return 0 - if argv is None: - argv = sys.argv - - import optparse - - parser = optparse.OptionParser( - usage="usage: %prog [options] path", - description="Display image data in TIFF files.", - version="%%prog %s" % __version__) - opt = parser.add_option - opt('-p', '--page', dest='page', type='int', default=-1, - help="display single page") - opt('-s', '--series', dest='series', type='int', default=-1, - help="display series of pages of same shape") - opt('--nomultifile', dest='nomultifile', action='store_true', - default=False, help="don't read OME series from multiple files") - opt('--noplot', dest='noplot', action='store_true', default=False, - help="don't display images") - opt('--interpol', dest='interpol', metavar='INTERPOL', default='bilinear', - help="image interpolation method") - opt('--dpi', dest='dpi', type='int', default=96, - help="set plot resolution") - opt('--debug', dest='debug', action='store_true', default=False, - help="raise exception on failures") - opt('--test', dest='test', action='store_true', default=False, - help="try read all images in path") - opt('--doctest', dest='doctest', action='store_true', default=False, - help="runs the docstring examples") - opt('-v', '--verbose', dest='verbose', action='store_true', default=True) - opt('-q', '--quiet', dest='verbose', action='store_false') - - settings, path = parser.parse_args() - path = ' '.join(path) - - if settings.doctest: - import doctest - doctest.testmod() - return 0 - if not path: - parser.error("No file specified") - if settings.test: - test_tifffile(path, settings.verbose) - return 0 - - if any(i in path for i in '?*'): - path = glob.glob(path) - if not path: - print('no files match the pattern') - return 0 - # TODO: handle image sequences - #if len(path) == 1: - path = path[0] - - print("Reading file structure...", end=' ') - start = time.time() - try: - tif = TiffFile(path, multifile=not settings.nomultifile) - except Exception as e: - if settings.debug: - raise - else: - print("\n", e) - sys.exit(0) - print("%.3f ms" % ((time.time()-start) * 1e3)) - - if tif.is_ome: - settings.norgb = True - - images = [(None, tif[0 if settings.page < 0 else settings.page])] - if not settings.noplot: - print("Reading image data... ", end=' ') - - def notnone(x): - return next(i for i in x if i is not None) - start = time.time() - try: - if settings.page >= 0: - images = [(tif.asarray(key=settings.page), - tif[settings.page])] - elif settings.series >= 0: - images = [(tif.asarray(series=settings.series), - notnone(tif.series[settings.series].pages))] - else: - images = [] - for i, s in enumerate(tif.series): - try: - images.append( - (tif.asarray(series=i), notnone(s.pages))) - except ValueError as e: - images.append((None, notnone(s.pages))) - if settings.debug: - raise - else: - print("\n* series %i failed: %s... " % (i, e), - end='') - print("%.3f ms" % ((time.time()-start) * 1e3)) - except Exception as e: - if settings.debug: - raise - else: - print(e) - - tif.close() - - print("\nTIFF file:", tif) - print() - for i, s in enumerate(tif.series): - print ("Series %i" % i) - print(s) - print() - for i, page in images: - print(page) - print(page.tags) - if page.is_palette: - print("\nColor Map:", page.color_map.shape, page.color_map.dtype) - for attr in ('cz_lsm_info', 'cz_lsm_scan_info', 'uic_tags', - 'mm_header', 'imagej_tags', 'micromanager_metadata', - 'nih_image_header'): - if hasattr(page, attr): - print("", attr.upper(), Record(getattr(page, attr)), sep="\n") - print() - if page.is_micromanager: - print('MICROMANAGER_FILE_METADATA') - print(Record(tif.micromanager_metadata)) - - if images and not settings.noplot: - try: - import matplotlib - matplotlib.use('TkAgg') - from matplotlib import pyplot - except ImportError as e: - warnings.warn("failed to import matplotlib.\n%s" % e) - else: - for img, page in images: - if img is None: - continue - vmin, vmax = None, None - if 'gdal_nodata' in page.tags: - try: - vmin = numpy.min(img[img > float(page.gdal_nodata)]) - except ValueError: - pass - if page.is_stk: - try: - vmin = page.uic_tags['min_scale'] - vmax = page.uic_tags['max_scale'] - except KeyError: - pass - else: - if vmax <= vmin: - vmin, vmax = None, None - title = "%s\n %s" % (str(tif), str(page)) - imshow(img, title=title, vmin=vmin, vmax=vmax, - bitspersample=page.bits_per_sample, - photometric=page.photometric, - interpolation=settings.interpol, - dpi=settings.dpi) - pyplot.show() - - -TIFFfile = TiffFile # backwards compatibility - -if sys.version_info[0] > 2: - basestring = str, bytes - unicode = str - -if __name__ == "__main__": - sys.exit(main()) - diff --git a/skimage/io/setup.py b/skimage/io/setup.py index dbe7e940..4e3c09c3 100644 --- a/skimage/io/setup.py +++ b/skimage/io/setup.py @@ -27,10 +27,6 @@ def configuration(parent_package='', top_path=None): sources=['_plugins/_histograms.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('_plugins._tifffile', - sources=['_plugins/tifffile.c'], - include_dirs=[get_numpy_include_dirs()]) - return config if __name__ == '__main__': From 24ac72a663d8753492c2448ecce2cb97a3bc1292 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 06:30:48 -0500 Subject: [PATCH 0573/1122] Bump the required tifffile version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 85f5a1f5..7ce05c95 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ scipy>=0.9 six>=1.3 networkx>=1.8 pillow>=1.1.7 -tifffile>=0.1 +tifffile>=0.2 From 56915d29c3e56fb1254d921d087d952a80d15c20 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 07:04:53 -0500 Subject: [PATCH 0574/1122] Fix tifffile imports --- skimage/io/_plugins/pil_plugin.py | 2 +- skimage/io/_plugins/tifffile_plugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index a23050db..885ce0c2 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -14,7 +14,7 @@ except ImportError: from skimage.util import img_as_ubyte, img_as_uint from six import string_types -from .tifffile import imread as tif_imread, imsave as tif_imsave +from tifffile import imread as tif_imread, imsave as tif_imsave def imread(fname, dtype=None): diff --git a/skimage/io/_plugins/tifffile_plugin.py b/skimage/io/_plugins/tifffile_plugin.py index fb0494c4..58038db4 100644 --- a/skimage/io/_plugins/tifffile_plugin.py +++ b/skimage/io/_plugins/tifffile_plugin.py @@ -1,5 +1,5 @@ try: - from .tifffile import imread, imsave + from tifffile import imread, imsave except ImportError: raise ImportError("The tifffile module could not be found.\n" "It can be obtained at " From 6cff910f674d9dfd9dce9c37bc856d67187a6017 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 15:12:45 -0500 Subject: [PATCH 0575/1122] Update the setup script to install tifffile --- tools/travis_setup.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh index f7602aef..c9f4c5c8 100755 --- a/tools/travis_setup.sh +++ b/tools/travis_setup.sh @@ -17,6 +17,8 @@ if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then pip install https://github.com/networkx/networkx/archive/networkx-1.8.tar.gz fi +pip install $WHEELHOUSE numpy +pip install tifffile pip install -r requirements.txt $WHEELHOUSE python check_bento_build.py From 7d171a8e37999725ee1a8c79fcd56a488dfcfffd Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 16:08:42 -0500 Subject: [PATCH 0576/1122] Fix install_requires --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f8169691..e96fe71e 100755 --- a/setup.py +++ b/setup.py @@ -142,11 +142,11 @@ if __name__ == "__main__": configuration=configuration, install_requires=[ - "six>=%s" % '.'.join(str(d) for d in DEPENDENCIES['six']) + "six>=%s" % DEPENDENCIES['six'] ], packages=setuptools.find_packages(exclude=['doc']), include_package_data=True, - zip_safe=False, # the package can run out of an .egg file + zip_safe=False, # the package can run out of an .egg file entry_points={ 'console_scripts': ['skivi = skimage.scripts.skivi:main'], From c74b652e1e85476b9fea232386b4628b42c86ee9 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 16:28:31 -0500 Subject: [PATCH 0577/1122] Remove install_requires altogether --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index e96fe71e..b4b74ab3 100755 --- a/setup.py +++ b/setup.py @@ -141,9 +141,6 @@ if __name__ == "__main__": ], configuration=configuration, - install_requires=[ - "six>=%s" % DEPENDENCIES['six'] - ], packages=setuptools.find_packages(exclude=['doc']), include_package_data=True, zip_safe=False, # the package can run out of an .egg file From d33833ae595810bde2ac51b0ee53ddff851449d8 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 16:54:37 -0500 Subject: [PATCH 0578/1122] Use retries where applicable --- .travis.yml | 2 +- tools/travis_test.sh | 42 ++++++++++++++++++++++++++++++++---------- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2b6451ca..8dc44224 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ python: - 3.4 before_install: - - tools/travis_setup.sh + - travis_retry tools/travis_setup.sh install: - python setup.py build_ext --inplace diff --git a/tools/travis_test.sh b/tools/travis_test.sh index df3a484d..3ed56785 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -ex + export DISPLAY=:99.0 export PYTHONWARNINGS="all" WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" @@ -13,17 +14,38 @@ flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples tools/header.py "Install optional dependencies" +# http://unix.stackexchange.com/a/82615 +function retry() +{ + local n=0 + local try=$3 + local cmd="${@: 1}" + [[ $# -le 1 ]] && { + echo "Usage $0 "; } + + until [[ $n -ge $try ]] + do + $cmd && break || { + echo "Command Fail.." + ((n++)) + echo "retry $n ::" + sleep 1; + } + + done +} + # Install Qt and then update the Matplotlib settings if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then - sudo apt-get install -q python-qt4 + retry sudo apt-get install -q python-qt4 MPL_QT_API=PyQt4 MPL_DIR=$HOME/.matplotlib export QT_API=pyqt else - sudo apt-get install -q libqt4-dev - pip install PySide $WHEELHOUSE - python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install + retry sudo apt-get install -q libqt4-dev + retry pip install PySide $WHEELHOUSE + retry python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install MPL_QT_API=PySide MPL_DIR=$HOME/.config/matplotlib export QT_API=pyside @@ -31,20 +53,20 @@ fi # imread does NOT support py3.2 if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then - sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools - pip install imread + retry sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools + retry pip install imread fi # TODO: update when SimpleITK become available on py34 or hopefully pip if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then - easy_install SimpleITK + retry easy_install SimpleITK fi -sudo apt-get install libfreeimage3 -pip install astropy +retry sudo apt-get install libfreeimage3 +retry pip install astropy if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then - pip install pyamg + retry pip install pyamg fi # Matplotlib settings - do not show figures during doc examples From 71233cca5155f0c6f1199c704304a0876c0d1dce Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 17:05:44 -0500 Subject: [PATCH 0579/1122] Break script into parts and travis_retry optional installs --- .travis.yml | 10 +++++ tools/travis_install_optional.sh | 38 ++++++++++++++++++ tools/travis_setup.sh | 2 - tools/travis_test.sh | 68 -------------------------------- 4 files changed, 48 insertions(+), 70 deletions(-) create mode 100644 tools/travis_install_optional.sh diff --git a/.travis.yml b/.travis.yml index 8dc44224..7a0e2b94 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,9 @@ python: - 3.4 before_install: + - export DISPLAY=:99.0 + - export PYTHONWARNINGS="all" + - export WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" - travis_retry tools/travis_setup.sh install: @@ -22,6 +25,13 @@ install: - python setup.py install script: + - tools/header.py "Run all tests with minimum dependencies" + - nosetests --exe -v skimage + + - tools/header.py "Pep8 and Flake tests" + - flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples + + - travis_retry tools/travis_install_optional.sh - tools/travis_test.sh after_success: diff --git a/tools/travis_install_optional.sh b/tools/travis_install_optional.sh new file mode 100644 index 00000000..c8d55fdf --- /dev/null +++ b/tools/travis_install_optional.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -ex + +tools/header.py "Install optional dependencies" + +# Install Qt and then update the Matplotlib settings +if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then + sudo apt-get install -q python-qt4 + MPL_QT_API=PyQt4 + MPL_DIR=$HOME/.matplotlib + export QT_API=pyqt + +else + sudo apt-get install -q libqt4-dev + pip install PySide $WHEELHOUSE + python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install + MPL_QT_API=PySide + MPL_DIR=$HOME/.config/matplotlib + export QT_API=pyside +fi + +# imread does NOT support py3.2 +if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then + sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools + pip install imread +fi + +# TODO: update when SimpleITK become available on py34 or hopefully pip +if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then + easy_install SimpleITK +fi + +sudo apt-get install libfreeimage3 +pip install astropy + +if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then + pip install pyamg +fi diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh index c9f4c5c8..cefe3a73 100755 --- a/tools/travis_setup.sh +++ b/tools/travis_setup.sh @@ -4,8 +4,6 @@ set -ex sh -e /etc/init.d/xvfb start sudo apt-get update -WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" - pip install wheel flake8 coveralls nose pip uninstall -y numpy diff --git a/tools/travis_test.sh b/tools/travis_test.sh index 3ed56785..3d35a9b1 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -1,74 +1,6 @@ #!/usr/bin/env bash set -ex - -export DISPLAY=:99.0 -export PYTHONWARNINGS="all" -WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" - -tools/header.py "Run all tests with minimum dependencies" -nosetests --exe -v skimage - -tools/header.py "Pep8 and Flake tests" -flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples - -tools/header.py "Install optional dependencies" - -# http://unix.stackexchange.com/a/82615 -function retry() -{ - local n=0 - local try=$3 - local cmd="${@: 1}" - [[ $# -le 1 ]] && { - echo "Usage $0 "; } - - until [[ $n -ge $try ]] - do - $cmd && break || { - echo "Command Fail.." - ((n++)) - echo "retry $n ::" - sleep 1; - } - - done -} - -# Install Qt and then update the Matplotlib settings -if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then - retry sudo apt-get install -q python-qt4 - MPL_QT_API=PyQt4 - MPL_DIR=$HOME/.matplotlib - export QT_API=pyqt - -else - retry sudo apt-get install -q libqt4-dev - retry pip install PySide $WHEELHOUSE - retry python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install - MPL_QT_API=PySide - MPL_DIR=$HOME/.config/matplotlib - export QT_API=pyside -fi - -# imread does NOT support py3.2 -if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then - retry sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools - retry pip install imread -fi - -# TODO: update when SimpleITK become available on py34 or hopefully pip -if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then - retry easy_install SimpleITK -fi - -retry sudo apt-get install libfreeimage3 -retry pip install astropy - -if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then - retry pip install pyamg -fi - # Matplotlib settings - do not show figures during doc examples mkdir -p $MPL_DIR touch $MPL_DIR/matplotlibrc From 479594653777c4ca9373cb25ddecd059e9e8ad7c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 17:20:23 -0500 Subject: [PATCH 0580/1122] Make new script executable --- tools/travis_install_optional.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 tools/travis_install_optional.sh diff --git a/tools/travis_install_optional.sh b/tools/travis_install_optional.sh old mode 100644 new mode 100755 From 0d2799a98dc5af0e776d32256b9926f47bd3eadb Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 18:35:35 -0500 Subject: [PATCH 0581/1122] Export the MPL variables --- tools/travis_install_optional.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/travis_install_optional.sh b/tools/travis_install_optional.sh index c8d55fdf..0aa24a42 100755 --- a/tools/travis_install_optional.sh +++ b/tools/travis_install_optional.sh @@ -6,16 +6,16 @@ tools/header.py "Install optional dependencies" # Install Qt and then update the Matplotlib settings if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then sudo apt-get install -q python-qt4 - MPL_QT_API=PyQt4 - MPL_DIR=$HOME/.matplotlib + export MPL_QT_API=PyQt4 + export MPL_DIR=$HOME/.matplotlib export QT_API=pyqt else sudo apt-get install -q libqt4-dev pip install PySide $WHEELHOUSE python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install - MPL_QT_API=PySide - MPL_DIR=$HOME/.config/matplotlib + export MPL_QT_API=PySide + export MPL_DIR=$HOME/.config/matplotlib export QT_API=pyside fi From 37d6ddfe363ab5946d0f7304aed45bfdb0316d3e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 19:11:32 -0500 Subject: [PATCH 0582/1122] Fix handling of variables - use local --- tools/travis_install_optional.sh | 6 ------ tools/travis_test.sh | 11 +++++++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/tools/travis_install_optional.sh b/tools/travis_install_optional.sh index 0aa24a42..a373cc6a 100755 --- a/tools/travis_install_optional.sh +++ b/tools/travis_install_optional.sh @@ -6,17 +6,11 @@ tools/header.py "Install optional dependencies" # Install Qt and then update the Matplotlib settings if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then sudo apt-get install -q python-qt4 - export MPL_QT_API=PyQt4 - export MPL_DIR=$HOME/.matplotlib - export QT_API=pyqt else sudo apt-get install -q libqt4-dev pip install PySide $WHEELHOUSE python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install - export MPL_QT_API=PySide - export MPL_DIR=$HOME/.config/matplotlib - export QT_API=pyside fi # imread does NOT support py3.2 diff --git a/tools/travis_test.sh b/tools/travis_test.sh index 3d35a9b1..4c2db692 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -1,6 +1,17 @@ #!/usr/bin/env bash set -ex +if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then + MPL_QT_API=PyQt4 + MPL_DIR=$HOME/.matplotlib + export QT_API=pyqt + +else + MPL_QT_API=PySide + MPL_DIR=$HOME/.config/matplotlib + export QT_API=pyside +fi + # Matplotlib settings - do not show figures during doc examples mkdir -p $MPL_DIR touch $MPL_DIR/matplotlibrc From 268eb5f2b54e8eebb0ba45063e660fd6b1092a1f Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 15 Oct 2014 05:57:05 -0500 Subject: [PATCH 0583/1122] Add tifffile to a new external package. --- skimage/external/tifffile/__init__.py | 4 + skimage/external/tifffile/_tifffile.py | 4839 ++++++++++++++++++++++++ skimage/io/_plugins/pil_plugin.py | 2 +- skimage/io/_plugins/tifffile_plugin.py | 7 +- 4 files changed, 4845 insertions(+), 7 deletions(-) create mode 100644 skimage/external/tifffile/__init__.py create mode 100644 skimage/external/tifffile/_tifffile.py diff --git a/skimage/external/tifffile/__init__.py b/skimage/external/tifffile/__init__.py new file mode 100644 index 00000000..da90abdf --- /dev/null +++ b/skimage/external/tifffile/__init__.py @@ -0,0 +1,4 @@ +try: + from tifffile import * +except ImportError: + from ._tifffile import * diff --git a/skimage/external/tifffile/_tifffile.py b/skimage/external/tifffile/_tifffile.py new file mode 100644 index 00000000..83ae4f0e --- /dev/null +++ b/skimage/external/tifffile/_tifffile.py @@ -0,0 +1,4839 @@ + + +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# tifffile.py + +# Copyright (c) 2008-2014, Christoph Gohlke +# Copyright (c) 2008-2014, The Regents of the University of California +# Produced at the Laboratory for Fluorescence Dynamics +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the copyright holders nor the names of any +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +"""Read and write image data from and to TIFF files. + +Image and metadata can be read from TIFF, BigTIFF, OME-TIFF, STK, LSM, NIH, +SGI, ImageJ, MicroManager, FluoView, SEQ and GEL files. +Only a subset of the TIFF specification is supported, mainly uncompressed +and losslessly compressed 2**(0 to 6) bit integer, 16, 32 and 64-bit float, +grayscale and RGB(A) images, which are commonly used in bio-scientific imaging. +Specifically, reading JPEG and CCITT compressed image data or EXIF, IPTC, GPS, +and XMP metadata is not implemented. +Only primary info records are read for STK, FluoView, MicroManager, and +NIH image formats. + +TIFF, the Tagged Image File Format, is under the control of Adobe Systems. +BigTIFF allows for files greater than 4 GB. STK, LSM, FluoView, SGI, SEQ, GEL, +and OME-TIFF, are custom extensions defined by Molecular Devices (Universal +Imaging Corporation), Carl Zeiss MicroImaging, Olympus, Silicon Graphics +International, Media Cybernetics, Molecular Dynamics, and the Open Microscopy +Environment consortium respectively. + +For command line usage run ``python tifffile.py --help`` + +:Author: + `Christoph Gohlke `_ + +:Organization: + Laboratory for Fluorescence Dynamics, University of California, Irvine + +:Version: 2014.08.24 + +Requirements +------------ +* `CPython 2.7 or 3.4 `_ +* `Numpy 1.8.2 `_ +* `Matplotlib 1.4 `_ (optional for plotting) +* `Tifffile.c 2013.11.05 `_ + (recommended for faster decoding of PackBits and LZW encoded strings) + +Notes +----- +The API is not stable yet and might change between revisions. + +Tested on little-endian platforms only. + +Other Python packages and modules for reading bio-scientific TIFF files: + +* `Imread `_ +* `PyLibTiff `_ +* `SimpleITK `_ +* `PyLSM `_ +* `PyMca.TiffIO.py `_ (same as fabio.TiffIO) +* `BioImageXD.Readers `_ +* `Cellcognition.io `_ +* `CellProfiler.bioformats + `_ + +Acknowledgements +---------------- +* Egor Zindy, University of Manchester, for cz_lsm_scan_info specifics. +* Wim Lewis for a bug fix and some read_cz_lsm functions. +* Hadrien Mary for help on reading MicroManager files. + +References +---------- +(1) TIFF 6.0 Specification and Supplements. Adobe Systems Incorporated. + http://partners.adobe.com/public/developer/tiff/ +(2) TIFF File Format FAQ. http://www.awaresystems.be/imaging/tiff/faq.html +(3) MetaMorph Stack (STK) Image File Format. + http://support.meta.moleculardevices.com/docs/t10243.pdf +(4) Image File Format Description LSM 5/7 Release 6.0 (ZEN 2010). + Carl Zeiss MicroImaging GmbH. BioSciences. May 10, 2011 +(5) File Format Description - LSM 5xx Release 2.0. + http://ibb.gsf.de/homepage/karsten.rodenacker/IDL/Lsmfile.doc +(6) The OME-TIFF format. + http://www.openmicroscopy.org/site/support/file-formats/ome-tiff +(7) UltraQuant(r) Version 6.0 for Windows Start-Up Guide. + http://www.ultralum.com/images%20ultralum/pdf/UQStart%20Up%20Guide.pdf +(8) Micro-Manager File Formats. + http://www.micro-manager.org/wiki/Micro-Manager_File_Formats +(9) Tags for TIFF and Related Specifications. Digital Preservation. + http://www.digitalpreservation.gov/formats/content/tiff_tags.shtml + +Examples +-------- +>>> data = numpy.random.rand(5, 301, 219) +>>> imsave('temp.tif', data) + +>>> image = imread('temp.tif') +>>> numpy.testing.assert_array_equal(image, data) + +>>> with TiffFile('temp.tif') as tif: +... images = tif.asarray() +... for page in tif: +... for tag in page.tags.values(): +... t = tag.name, tag.value +... image = page.asarray() + +""" + +from __future__ import division, print_function + +import sys +import os +import re +import glob +import math +import zlib +import time +import json +import struct +import warnings +import tempfile +import datetime +import collections +from fractions import Fraction +from xml.etree import cElementTree as etree + +import numpy + +__version__ = '0.3.1' +__docformat__ = 'restructuredtext en' +__all__ = ('imsave', 'imread', 'imshow', 'TiffFile', 'TiffWriter', + 'TiffSequence') + + +def imsave(filename, data, **kwargs): + """Write image data to TIFF file. + + Refer to the TiffWriter class and member functions for documentation. + + Parameters + ---------- + filename : str + Name of file to write. + data : array_like + Input image. The last dimensions are assumed to be image depth, + height, width, and samples. + kwargs : dict + Parameters 'byteorder', 'bigtiff', and 'software' are passed to + the TiffWriter class. + Parameters 'photometric', 'planarconfig', 'resolution', + 'description', 'compress', 'volume', and 'extratags' are passed to + the TiffWriter.save function. + + Examples + -------- + >>> data = numpy.random.rand(2, 5, 3, 301, 219) + >>> description = u'{"shape": %s}' % str(list(data.shape)) + >>> imsave('temp.tif', data, compress=6, + ... extratags=[(270, 's', 0, description, True)]) + + """ + tifargs = {} + for key in ('byteorder', 'bigtiff', 'software', 'writeshape'): + if key in kwargs: + tifargs[key] = kwargs[key] + del kwargs[key] + + if 'writeshape' not in kwargs: + kwargs['writeshape'] = True + if 'bigtiff' not in tifargs and data.size*data.dtype.itemsize > 2000*2**20: + tifargs['bigtiff'] = True + + with TiffWriter(filename, **tifargs) as tif: + tif.save(data, **kwargs) + + +class TiffWriter(object): + """Write image data to TIFF file. + + TiffWriter instances must be closed using the close method, which is + automatically called when using the 'with' statement. + + Examples + -------- + >>> data = numpy.random.rand(2, 5, 3, 301, 219) + >>> with TiffWriter('temp.tif', bigtiff=True) as tif: + ... for i in range(data.shape[0]): + ... tif.save(data[i], compress=6) + + """ + TYPES = {'B': 1, 's': 2, 'H': 3, 'I': 4, '2I': 5, 'b': 6, + 'h': 8, 'i': 9, 'f': 11, 'd': 12, 'Q': 16, 'q': 17} + TAGS = { + 'new_subfile_type': 254, 'subfile_type': 255, + 'image_width': 256, 'image_length': 257, 'bits_per_sample': 258, + 'compression': 259, 'photometric': 262, 'fill_order': 266, + 'document_name': 269, 'image_description': 270, 'strip_offsets': 273, + 'orientation': 274, 'samples_per_pixel': 277, 'rows_per_strip': 278, + 'strip_byte_counts': 279, 'x_resolution': 282, 'y_resolution': 283, + 'planar_configuration': 284, 'page_name': 285, 'resolution_unit': 296, + 'software': 305, 'datetime': 306, 'predictor': 317, 'color_map': 320, + 'tile_width': 322, 'tile_length': 323, 'tile_offsets': 324, + 'tile_byte_counts': 325, 'extra_samples': 338, 'sample_format': 339, + 'image_depth': 32997, 'tile_depth': 32998} + + def __init__(self, filename, bigtiff=False, byteorder=None, + software='tifffile.py'): + """Create a new TIFF file for writing. + + Use bigtiff=True when creating files greater than 2 GB. + + Parameters + ---------- + filename : str + Name of file to write. + bigtiff : bool + If True, the BigTIFF format is used. + byteorder : {'<', '>'} + The endianness of the data in the file. + By default this is the system's native byte order. + software : str + Name of the software used to create the image. + Saved with the first page only. + + """ + if byteorder not in (None, '<', '>'): + raise ValueError("invalid byteorder %s" % byteorder) + if byteorder is None: + byteorder = '<' if sys.byteorder == 'little' else '>' + + self._byteorder = byteorder + self._software = software + + self._fh = open(filename, 'wb') + self._fh.write({'<': b'II', '>': b'MM'}[byteorder]) + + if bigtiff: + self._bigtiff = True + self._offset_size = 8 + self._tag_size = 20 + self._numtag_format = 'Q' + self._offset_format = 'Q' + self._val_format = '8s' + self._fh.write(struct.pack(byteorder+'HHH', 43, 8, 0)) + else: + self._bigtiff = False + self._offset_size = 4 + self._tag_size = 12 + self._numtag_format = 'H' + self._offset_format = 'I' + self._val_format = '4s' + self._fh.write(struct.pack(byteorder+'H', 42)) + + # first IFD + self._ifd_offset = self._fh.tell() + self._fh.write(struct.pack(byteorder+self._offset_format, 0)) + + def save(self, data, photometric=None, planarconfig=None, resolution=None, + description=None, volume=False, writeshape=False, compress=0, + extratags=()): + """Write image data to TIFF file. + + Image data are written in one stripe per plane. + Dimensions larger than 2 to 4 (depending on photometric mode, planar + configuration, and SGI mode) are flattened and saved as separate pages. + The 'sample_format' and 'bits_per_sample' TIFF tags are derived from + the data type. + + Parameters + ---------- + data : array_like + Input image. The last dimensions are assumed to be image depth, + height, width, and samples. + photometric : {'minisblack', 'miniswhite', 'rgb'} + The color space of the image data. + By default this setting is inferred from the data shape. + planarconfig : {'contig', 'planar'} + Specifies if samples are stored contiguous or in separate planes. + By default this setting is inferred from the data shape. + 'contig': last dimension contains samples. + 'planar': third last dimension contains samples. + resolution : (float, float) or ((int, int), (int, int)) + X and Y resolution in dots per inch as float or rational numbers. + description : str + The subject of the image. Saved with the first page only. + compress : int + Values from 0 to 9 controlling the level of zlib compression. + If 0, data are written uncompressed (default). + volume : bool + If True, volume data are stored in one tile (if applicable) using + the SGI image_depth and tile_depth tags. + Image width and depth must be multiple of 16. + Few software can read this format, e.g. MeVisLab. + writeshape : bool + If True, write the data shape to the image_description tag + if necessary and no other description is given. + extratags: sequence of tuples + Additional tags as [(code, dtype, count, value, writeonce)]. + + code : int + The TIFF tag Id. + dtype : str + Data type of items in 'value' in Python struct format. + One of B, s, H, I, 2I, b, h, i, f, d, Q, or q. + count : int + Number of data values. Not used for string values. + value : sequence + 'Count' values compatible with 'dtype'. + writeonce : bool + If True, the tag is written to the first page only. + + """ + if photometric not in (None, 'minisblack', 'miniswhite', 'rgb'): + raise ValueError("invalid photometric %s" % photometric) + if planarconfig not in (None, 'contig', 'planar'): + raise ValueError("invalid planarconfig %s" % planarconfig) + if not 0 <= compress <= 9: + raise ValueError("invalid compression level %s" % compress) + + fh = self._fh + byteorder = self._byteorder + numtag_format = self._numtag_format + val_format = self._val_format + offset_format = self._offset_format + offset_size = self._offset_size + tag_size = self._tag_size + + data = numpy.asarray(data, dtype=byteorder+data.dtype.char, order='C') + data_shape = shape = data.shape + data = numpy.atleast_2d(data) + + # normalize shape of data + samplesperpixel = 1 + extrasamples = 0 + if volume and data.ndim < 3: + volume = False + if photometric is None: + if planarconfig: + photometric = 'rgb' + elif data.ndim > 2 and shape[-1] in (3, 4): + photometric = 'rgb' + elif volume and data.ndim > 3 and shape[-4] in (3, 4): + photometric = 'rgb' + elif data.ndim > 2 and shape[-3] in (3, 4): + photometric = 'rgb' + else: + photometric = 'minisblack' + if planarconfig and len(shape) <= (3 if volume else 2): + planarconfig = None + photometric = 'minisblack' + if photometric == 'rgb': + if len(shape) < 3: + raise ValueError("not a RGB(A) image") + if len(shape) < 4: + volume = False + if planarconfig is None: + if shape[-1] in (3, 4): + planarconfig = 'contig' + elif shape[-4 if volume else -3] in (3, 4): + planarconfig = 'planar' + elif shape[-1] > shape[-4 if volume else -3]: + planarconfig = 'planar' + else: + planarconfig = 'contig' + if planarconfig == 'contig': + data = data.reshape((-1, 1) + shape[(-4 if volume else -3):]) + samplesperpixel = data.shape[-1] + else: + data = data.reshape( + (-1,) + shape[(-4 if volume else -3):] + (1,)) + samplesperpixel = data.shape[1] + if samplesperpixel > 3: + extrasamples = samplesperpixel - 3 + elif planarconfig and len(shape) > (3 if volume else 2): + if planarconfig == 'contig': + data = data.reshape((-1, 1) + shape[(-4 if volume else -3):]) + samplesperpixel = data.shape[-1] + else: + data = data.reshape( + (-1,) + shape[(-4 if volume else -3):] + (1,)) + samplesperpixel = data.shape[1] + extrasamples = samplesperpixel - 1 + else: + planarconfig = None + # remove trailing 1s + while len(shape) > 2 and shape[-1] == 1: + shape = shape[:-1] + if len(shape) < 3: + volume = False + if False and ( + len(shape) > (3 if volume else 2) and shape[-1] < 5 and + all(shape[-1] < i + for i in shape[(-4 if volume else -3):-1])): + # DISABLED: non-standard TIFF, e.g. (220, 320, 2) + planarconfig = 'contig' + samplesperpixel = shape[-1] + data = data.reshape((-1, 1) + shape[(-4 if volume else -3):]) + else: + data = data.reshape( + (-1, 1) + shape[(-3 if volume else -2):] + (1,)) + + if samplesperpixel == 2: + warnings.warn("writing non-standard TIFF (samplesperpixel 2)") + + if volume and (data.shape[-2] % 16 or data.shape[-3] % 16): + warnings.warn("volume width or length are not multiple of 16") + volume = False + data = numpy.swapaxes(data, 1, 2) + data = data.reshape( + (data.shape[0] * data.shape[1],) + data.shape[2:]) + + # data.shape is now normalized 5D or 6D, depending on volume + # (pages, planar_samples, (depth,) height, width, contig_samples) + assert len(data.shape) in (5, 6) + shape = data.shape + + bytestr = bytes if sys.version[0] == '2' else ( + lambda x: bytes(x, 'utf-8') if isinstance(x, str) else x) + tags = [] # list of (code, ifdentry, ifdvalue, writeonce) + + if volume: + # use tiles to save volume data + tag_byte_counts = TiffWriter.TAGS['tile_byte_counts'] + tag_offsets = TiffWriter.TAGS['tile_offsets'] + else: + # else use strips + tag_byte_counts = TiffWriter.TAGS['strip_byte_counts'] + tag_offsets = TiffWriter.TAGS['strip_offsets'] + + def pack(fmt, *val): + return struct.pack(byteorder+fmt, *val) + + def addtag(code, dtype, count, value, writeonce=False): + # Compute ifdentry & ifdvalue bytes from code, dtype, count, value. + # Append (code, ifdentry, ifdvalue, writeonce) to tags list. + code = int(TiffWriter.TAGS.get(code, code)) + try: + tifftype = TiffWriter.TYPES[dtype] + except KeyError: + raise ValueError("unknown dtype %s" % dtype) + rawcount = count + if dtype == 's': + value = bytestr(value) + b'\0' + count = rawcount = len(value) + value = (value, ) + if len(dtype) > 1: + count *= int(dtype[:-1]) + dtype = dtype[-1] + ifdentry = [pack('HH', code, tifftype), + pack(offset_format, rawcount)] + ifdvalue = None + if count == 1: + if isinstance(value, (tuple, list)): + value = value[0] + ifdentry.append(pack(val_format, pack(dtype, value))) + elif struct.calcsize(dtype) * count <= offset_size: + ifdentry.append(pack(val_format, + pack(str(count)+dtype, *value))) + else: + ifdentry.append(pack(offset_format, 0)) + ifdvalue = pack(str(count)+dtype, *value) + tags.append((code, b''.join(ifdentry), ifdvalue, writeonce)) + + def rational(arg, max_denominator=1000000): + # return nominator and denominator from float or two integers + try: + f = Fraction.from_float(arg) + except TypeError: + f = Fraction(arg[0], arg[1]) + f = f.limit_denominator(max_denominator) + return f.numerator, f.denominator + + if self._software: + addtag('software', 's', 0, self._software, writeonce=True) + self._software = None # only save to first page + if description: + addtag('image_description', 's', 0, description, writeonce=True) + elif writeshape and shape[0] > 1 and shape != data_shape: + addtag('image_description', 's', 0, + "shape=(%s)" % (",".join('%i' % i for i in data_shape)), + writeonce=True) + addtag('datetime', 's', 0, + datetime.datetime.now().strftime("%Y:%m:%d %H:%M:%S"), + writeonce=True) + addtag('compression', 'H', 1, 32946 if compress else 1) + addtag('orientation', 'H', 1, 1) + addtag('image_width', 'I', 1, shape[-2]) + addtag('image_length', 'I', 1, shape[-3]) + if volume: + addtag('image_depth', 'I', 1, shape[-4]) + addtag('tile_depth', 'I', 1, shape[-4]) + addtag('tile_width', 'I', 1, shape[-2]) + addtag('tile_length', 'I', 1, shape[-3]) + addtag('new_subfile_type', 'I', 1, 0 if shape[0] == 1 else 2) + addtag('sample_format', 'H', 1, + {'u': 1, 'i': 2, 'f': 3, 'c': 6}[data.dtype.kind]) + addtag('photometric', 'H', 1, + {'miniswhite': 0, 'minisblack': 1, 'rgb': 2}[photometric]) + addtag('samples_per_pixel', 'H', 1, samplesperpixel) + if planarconfig and samplesperpixel > 1: + addtag('planar_configuration', 'H', 1, 1 + if planarconfig == 'contig' else 2) + addtag('bits_per_sample', 'H', samplesperpixel, + (data.dtype.itemsize * 8, ) * samplesperpixel) + else: + addtag('bits_per_sample', 'H', 1, data.dtype.itemsize * 8) + if extrasamples: + if photometric == 'rgb' and extrasamples == 1: + addtag('extra_samples', 'H', 1, 1) # associated alpha channel + else: + addtag('extra_samples', 'H', extrasamples, (0,) * extrasamples) + if resolution: + addtag('x_resolution', '2I', 1, rational(resolution[0])) + addtag('y_resolution', '2I', 1, rational(resolution[1])) + addtag('resolution_unit', 'H', 1, 2) + addtag('rows_per_strip', 'I', 1, + shape[-3] * (shape[-4] if volume else 1)) + + # use one strip or tile per plane + strip_byte_counts = (data[0, 0].size * data.dtype.itemsize,) * shape[1] + addtag(tag_byte_counts, offset_format, shape[1], strip_byte_counts) + addtag(tag_offsets, offset_format, shape[1], (0, ) * shape[1]) + + # add extra tags from users + for t in extratags: + addtag(*t) + # the entries in an IFD must be sorted in ascending order by tag code + tags = sorted(tags, key=lambda x: x[0]) + + if not self._bigtiff and (fh.tell() + data.size*data.dtype.itemsize + > 2**31-1): + raise ValueError("data too large for non-bigtiff file") + + for pageindex in range(shape[0]): + # update pointer at ifd_offset + pos = fh.tell() + fh.seek(self._ifd_offset) + fh.write(pack(offset_format, pos)) + fh.seek(pos) + + # write ifdentries + fh.write(pack(numtag_format, len(tags))) + tag_offset = fh.tell() + fh.write(b''.join(t[1] for t in tags)) + self._ifd_offset = fh.tell() + fh.write(pack(offset_format, 0)) # offset to next IFD + + # write tag values and patch offsets in ifdentries, if necessary + for tagindex, tag in enumerate(tags): + if tag[2]: + pos = fh.tell() + fh.seek(tag_offset + tagindex*tag_size + offset_size + 4) + fh.write(pack(offset_format, pos)) + fh.seek(pos) + if tag[0] == tag_offsets: + strip_offsets_offset = pos + elif tag[0] == tag_byte_counts: + strip_byte_counts_offset = pos + fh.write(tag[2]) + + # write image data + data_offset = fh.tell() + if compress: + strip_byte_counts = [] + for plane in data[pageindex]: + plane = zlib.compress(plane, compress) + strip_byte_counts.append(len(plane)) + fh.write(plane) + else: + # if this fails try update Python/numpy + data[pageindex].tofile(fh) + fh.flush() + + # update strip and tile offsets and byte_counts if necessary + pos = fh.tell() + for tagindex, tag in enumerate(tags): + if tag[0] == tag_offsets: # strip or tile offsets + if tag[2]: + fh.seek(strip_offsets_offset) + strip_offset = data_offset + for size in strip_byte_counts: + fh.write(pack(offset_format, strip_offset)) + strip_offset += size + else: + fh.seek(tag_offset + tagindex*tag_size + + offset_size + 4) + fh.write(pack(offset_format, data_offset)) + elif tag[0] == tag_byte_counts: # strip or tile byte_counts + if compress: + if tag[2]: + fh.seek(strip_byte_counts_offset) + for size in strip_byte_counts: + fh.write(pack(offset_format, size)) + else: + fh.seek(tag_offset + tagindex*tag_size + + offset_size + 4) + fh.write(pack(offset_format, strip_byte_counts[0])) + break + fh.seek(pos) + fh.flush() + # remove tags that should be written only once + if pageindex == 0: + tags = [t for t in tags if not t[-1]] + + def close(self): + self._fh.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + +def imread(files, **kwargs): + """Return image data from TIFF file(s) as numpy array. + + The first image series is returned if no arguments are provided. + + Parameters + ---------- + files : str or list + File name, glob pattern, or list of file names. + key : int, slice, or sequence of page indices + Defines which pages to return as array. + series : int + Defines which series of pages in file to return as array. + multifile : bool + If True (default), OME-TIFF data may include pages from multiple files. + pattern : str + Regular expression pattern that matches axes names and indices in + file names. + kwargs : dict + Additional parameters passed to the TiffFile or TiffSequence asarray + function. + + Examples + -------- + >>> im = imread('test.tif', key=0) + >>> im.shape + (256, 256, 4) + >>> ims = imread(['test.tif', 'test.tif']) + >>> ims.shape + (2, 256, 256, 4) + + """ + kwargs_file = {} + if 'multifile' in kwargs: + kwargs_file['multifile'] = kwargs['multifile'] + del kwargs['multifile'] + else: + kwargs_file['multifile'] = True + kwargs_seq = {} + if 'pattern' in kwargs: + kwargs_seq['pattern'] = kwargs['pattern'] + del kwargs['pattern'] + + if isinstance(files, basestring) and any(i in files for i in '?*'): + files = glob.glob(files) + if not files: + raise ValueError('no files found') + if len(files) == 1: + files = files[0] + + if isinstance(files, basestring): + with TiffFile(files, **kwargs_file) as tif: + return tif.asarray(**kwargs) + else: + with TiffSequence(files, **kwargs_seq) as imseq: + return imseq.asarray(**kwargs) + + +class lazyattr(object): + """Lazy object attribute whose value is computed on first access.""" + __slots__ = ('func', ) + + def __init__(self, func): + self.func = func + + def __get__(self, instance, owner): + if instance is None: + return self + value = self.func(instance) + if value is NotImplemented: + return getattr(super(owner, instance), self.func.__name__) + setattr(instance, self.func.__name__, value) + return value + + +class TiffFile(object): + """Read image and metadata from TIFF, STK, LSM, and FluoView files. + + TiffFile instances must be closed using the close method, which is + automatically called when using the 'with' statement. + + Attributes + ---------- + pages : list + All TIFF pages in file. + series : list of Records(shape, dtype, axes, TiffPages) + TIFF pages with compatible shapes and types. + micromanager_metadata: dict + Extra MicroManager non-TIFF metadata in the file, if exists. + + All attributes are read-only. + + Examples + -------- + >>> with TiffFile('test.tif') as tif: + ... data = tif.asarray() + ... data.shape + (256, 256, 4) + + """ + def __init__(self, arg, name=None, offset=None, size=None, + multifile=True, multifile_close=True): + """Initialize instance from file. + + Parameters + ---------- + arg : str or open file + Name of file or open file object. + The file objects are closed in TiffFile.close(). + name : str + Optional name of file in case 'arg' is a file handle. + offset : int + Optional start position of embedded file. By default this is + the current file position. + size : int + Optional size of embedded file. By default this is the number + of bytes from the 'offset' to the end of the file. + multifile : bool + If True (default), series may include pages from multiple files. + Currently applies to OME-TIFF only. + multifile_close : bool + If True (default), keep the handles of other files in multifile + series closed. This is inefficient when few files refer to + many pages. If False, the C runtime may run out of resources. + + """ + self._fh = FileHandle(arg, name=name, offset=offset, size=size) + self.offset_size = None + self.pages = [] + self._multifile = bool(multifile) + self._multifile_close = bool(multifile_close) + self._files = {self._fh.name: self} # cache of TiffFiles + try: + self._fromfile() + except Exception: + self._fh.close() + raise + + @property + def filehandle(self): + """Return file handle.""" + return self._fh + + @property + def filename(self): + """Return name of file handle.""" + return self._fh.name + + def close(self): + """Close open file handle(s).""" + for tif in self._files.values(): + tif._fh.close() + self._files = {} + + def _fromfile(self): + """Read TIFF header and all page records from file.""" + self._fh.seek(0) + try: + self.byteorder = {b'II': '<', b'MM': '>'}[self._fh.read(2)] + except KeyError: + raise ValueError("not a valid TIFF file") + version = struct.unpack(self.byteorder+'H', self._fh.read(2))[0] + if version == 43: # BigTiff + self.offset_size, zero = struct.unpack(self.byteorder+'HH', + self._fh.read(4)) + if zero or self.offset_size != 8: + raise ValueError("not a valid BigTIFF file") + elif version == 42: + self.offset_size = 4 + else: + raise ValueError("not a TIFF file") + self.pages = [] + while True: + try: + page = TiffPage(self) + self.pages.append(page) + except StopIteration: + break + if not self.pages: + raise ValueError("empty TIFF file") + + if self.is_micromanager: + # MicroManager files contain metadata not stored in TIFF tags. + self.micromanager_metadata = read_micromanager_metadata(self._fh) + + if self.is_lsm: + self._fix_lsm_strip_offsets() + self._fix_lsm_strip_byte_counts() + + def _fix_lsm_strip_offsets(self): + """Unwrap strip offsets for LSM files greater than 4 GB.""" + for series in self.series: + wrap = 0 + previous_offset = 0 + for page in series.pages: + strip_offsets = [] + for current_offset in page.strip_offsets: + if current_offset < previous_offset: + wrap += 2**32 + strip_offsets.append(current_offset + wrap) + previous_offset = current_offset + page.strip_offsets = tuple(strip_offsets) + + def _fix_lsm_strip_byte_counts(self): + """Set strip_byte_counts to size of compressed data. + + The strip_byte_counts tag in LSM files contains the number of bytes + for the uncompressed data. + + """ + if not self.pages: + return + strips = {} + for page in self.pages: + assert len(page.strip_offsets) == len(page.strip_byte_counts) + for offset, bytecount in zip(page.strip_offsets, + page.strip_byte_counts): + strips[offset] = bytecount + offsets = sorted(strips.keys()) + offsets.append(min(offsets[-1] + strips[offsets[-1]], self._fh.size)) + for i, offset in enumerate(offsets[:-1]): + strips[offset] = min(strips[offset], offsets[i+1] - offset) + for page in self.pages: + if page.compression: + page.strip_byte_counts = tuple( + strips[offset] for offset in page.strip_offsets) + + @lazyattr + def series(self): + """Return series of TiffPage with compatible shape and properties.""" + if not self.pages: + return [] + + series = [] + page0 = self.pages[0] + + if self.is_ome: + series = self._omeseries() + elif self.is_fluoview: + dims = {b'X': 'X', b'Y': 'Y', b'Z': 'Z', b'T': 'T', + b'WAVELENGTH': 'C', b'TIME': 'T', b'XY': 'R', + b'EVENT': 'V', b'EXPOSURE': 'L'} + mmhd = list(reversed(page0.mm_header.dimensions)) + series = [Record( + axes=''.join(dims.get(i[0].strip().upper(), 'Q') + for i in mmhd if i[1] > 1), + shape=tuple(int(i[1]) for i in mmhd if i[1] > 1), + pages=self.pages, dtype=numpy.dtype(page0.dtype))] + elif self.is_lsm: + lsmi = page0.cz_lsm_info + axes = CZ_SCAN_TYPES[lsmi.scan_type] + if page0.is_rgb: + axes = axes.replace('C', '').replace('XY', 'XYC') + axes = axes[::-1] + shape = tuple(getattr(lsmi, CZ_DIMENSIONS[i]) for i in axes) + pages = [p for p in self.pages if not p.is_reduced] + series = [Record(axes=axes, shape=shape, pages=pages, + dtype=numpy.dtype(pages[0].dtype))] + if len(pages) != len(self.pages): # reduced RGB pages + pages = [p for p in self.pages if p.is_reduced] + cp = 1 + i = 0 + while cp < len(pages) and i < len(shape)-2: + cp *= shape[i] + i += 1 + shape = shape[:i] + pages[0].shape + axes = axes[:i] + 'CYX' + series.append(Record(axes=axes, shape=shape, pages=pages, + dtype=numpy.dtype(pages[0].dtype))) + elif self.is_imagej: + shape = [] + axes = [] + ij = page0.imagej_tags + if 'frames' in ij: + shape.append(ij['frames']) + axes.append('T') + if 'slices' in ij: + shape.append(ij['slices']) + axes.append('Z') + if 'channels' in ij and not self.is_rgb: + shape.append(ij['channels']) + axes.append('C') + remain = len(self.pages) // (product(shape) if shape else 1) + if remain > 1: + shape.append(remain) + axes.append('I') + shape.extend(page0.shape) + axes.extend(page0.axes) + axes = ''.join(axes) + series = [Record(pages=self.pages, shape=tuple(shape), axes=axes, + dtype=numpy.dtype(page0.dtype))] + elif self.is_nih: + if len(self.pages) == 1: + shape = page0.shape + axes = page0.axes + else: + shape = (len(self.pages),) + page0.shape + axes = 'I' + page0.axes + series = [Record(pages=self.pages, shape=shape, axes=axes, + dtype=numpy.dtype(page0.dtype))] + elif page0.is_shaped: + # TODO: shaped files can contain multiple series + shape = page0.tags['image_description'].value[7:-1] + shape = tuple(int(i) for i in shape.split(b',')) + series = [Record(pages=self.pages, shape=shape, + axes='Q' * len(shape), + dtype=numpy.dtype(page0.dtype))] + + # generic detection of series + if not series: + shapes = [] + pages = {} + for page in self.pages: + if not page.shape: + continue + shape = page.shape + (page.axes, + page.compression in TIFF_DECOMPESSORS) + if shape not in pages: + shapes.append(shape) + pages[shape] = [page] + else: + pages[shape].append(page) + series = [Record(pages=pages[s], + axes=(('I' + s[-2]) + if len(pages[s]) > 1 else s[-2]), + dtype=numpy.dtype(pages[s][0].dtype), + shape=((len(pages[s]), ) + s[:-2] + if len(pages[s]) > 1 else s[:-2])) + for s in shapes] + + # remove empty series, e.g. in MD Gel files + series = [s for s in series if sum(s.shape) > 0] + + return series + + def asarray(self, key=None, series=None, memmap=False): + """Return image data from multiple TIFF pages as numpy array. + + By default the first image series is returned. + + Parameters + ---------- + key : int, slice, or sequence of page indices + Defines which pages to return as array. + series : int + Defines which series of pages to return as array. + memmap : bool + If True, return an array stored in a binary file on disk + if possible. + + """ + if key is None and series is None: + series = 0 + if series is not None: + pages = self.series[series].pages + else: + pages = self.pages + + if key is None: + pass + elif isinstance(key, int): + pages = [pages[key]] + elif isinstance(key, slice): + pages = pages[key] + elif isinstance(key, collections.Iterable): + pages = [pages[k] for k in key] + else: + raise TypeError("key must be an int, slice, or sequence") + + if not len(pages): + raise ValueError("no pages selected") + + if self.is_nih: + if pages[0].is_palette: + result = stack_pages(pages, colormapped=False, squeeze=False) + result = numpy.take(pages[0].color_map, result, axis=1) + result = numpy.swapaxes(result, 0, 1) + else: + result = stack_pages(pages, memmap=memmap, + colormapped=False, squeeze=False) + elif len(pages) == 1: + return pages[0].asarray(memmap=memmap) + elif self.is_ome: + assert not self.is_palette, "color mapping disabled for ome-tiff" + if any(p is None for p in pages): + # zero out missing pages + firstpage = next(p for p in pages if p) + nopage = numpy.zeros_like( + firstpage.asarray(memmap=False)) + s = self.series[series] + if memmap: + with tempfile.NamedTemporaryFile() as fh: + result = numpy.memmap(fh, dtype=s.dtype, shape=s.shape) + result = result.reshape(-1) + else: + result = numpy.empty(s.shape, s.dtype).reshape(-1) + index = 0 + + class KeepOpen: + # keep Tiff files open between consecutive pages + def __init__(self, parent, close): + self.master = parent + self.parent = parent + self._close = close + + def open(self, page): + if self._close and page and page.parent != self.parent: + if self.parent != self.master: + self.parent.filehandle.close() + self.parent = page.parent + self.parent.filehandle.open() + + def close(self): + if self._close and self.parent != self.master: + self.parent.filehandle.close() + + keep = KeepOpen(self, self._multifile_close) + for page in pages: + keep.open(page) + if page: + a = page.asarray(memmap=False, colormapped=False, + reopen=False) + else: + a = nopage + try: + result[index:index + a.size] = a.reshape(-1) + except ValueError as e: + warnings.warn("ome-tiff: %s" % e) + break + index += a.size + keep.close() + else: + result = stack_pages(pages, memmap=memmap) + + if key is None: + try: + result.shape = self.series[series].shape + except ValueError: + try: + warnings.warn("failed to reshape %s to %s" % ( + result.shape, self.series[series].shape)) + # try series of expected shapes + result.shape = (-1,) + self.series[series].shape + except ValueError: + # revert to generic shape + result.shape = (-1,) + pages[0].shape + else: + result.shape = (-1,) + pages[0].shape + return result + + def _omeseries(self): + """Return image series in OME-TIFF file(s).""" + root = etree.fromstring(self.pages[0].tags['image_description'].value) + uuid = root.attrib.get('UUID', None) + self._files = {uuid: self} + dirname = self._fh.dirname + modulo = {} + result = [] + for element in root: + if element.tag.endswith('BinaryOnly'): + warnings.warn("ome-xml: not an ome-tiff master file") + break + if element.tag.endswith('StructuredAnnotations'): + for annot in element: + if not annot.attrib.get('Namespace', + '').endswith('modulo'): + continue + for value in annot: + for modul in value: + for along in modul: + if not along.tag[:-1].endswith('Along'): + continue + axis = along.tag[-1] + newaxis = along.attrib.get('Type', 'other') + newaxis = AXES_LABELS[newaxis] + if 'Start' in along.attrib: + labels = range( + int(along.attrib['Start']), + int(along.attrib['End']) + 1, + int(along.attrib.get('Step', 1))) + else: + labels = [label.text for label in along + if label.tag.endswith('Label')] + modulo[axis] = (newaxis, labels) + if not element.tag.endswith('Image'): + continue + for pixels in element: + if not pixels.tag.endswith('Pixels'): + continue + atr = pixels.attrib + dtype = atr.get('Type', None) + axes = ''.join(reversed(atr['DimensionOrder'])) + shape = list(int(atr['Size'+ax]) for ax in axes) + size = product(shape[:-2]) + ifds = [None] * size + for data in pixels: + if not data.tag.endswith('TiffData'): + continue + atr = data.attrib + ifd = int(atr.get('IFD', 0)) + num = int(atr.get('NumPlanes', 1 if 'IFD' in atr else 0)) + num = int(atr.get('PlaneCount', num)) + idx = [int(atr.get('First'+ax, 0)) for ax in axes[:-2]] + try: + idx = numpy.ravel_multi_index(idx, shape[:-2]) + except ValueError: + # ImageJ produces invalid ome-xml when cropping + warnings.warn("ome-xml: invalid TiffData index") + continue + for uuid in data: + if not uuid.tag.endswith('UUID'): + continue + if uuid.text not in self._files: + if not self._multifile: + # abort reading multifile OME series + # and fall back to generic series + return [] + fname = uuid.attrib['FileName'] + try: + tif = TiffFile(os.path.join(dirname, fname)) + except (IOError, ValueError): + tif.close() + warnings.warn( + "ome-xml: failed to read '%s'" % fname) + break + self._files[uuid.text] = tif + if self._multifile_close: + tif.close() + pages = self._files[uuid.text].pages + try: + for i in range(num if num else len(pages)): + ifds[idx + i] = pages[ifd + i] + except IndexError: + warnings.warn("ome-xml: index out of range") + # only process first uuid + break + else: + pages = self.pages + try: + for i in range(num if num else len(pages)): + ifds[idx + i] = pages[ifd + i] + except IndexError: + warnings.warn("ome-xml: index out of range") + if all(i is None for i in ifds): + # skip images without data + continue + dtype = next(i for i in ifds if i).dtype + result.append(Record(axes=axes, shape=shape, pages=ifds, + dtype=numpy.dtype(dtype))) + + for record in result: + for axis, (newaxis, labels) in modulo.items(): + i = record.axes.index(axis) + size = len(labels) + if record.shape[i] == size: + record.axes = record.axes.replace(axis, newaxis, 1) + else: + record.shape[i] //= size + record.shape.insert(i+1, size) + record.axes = record.axes.replace(axis, axis+newaxis, 1) + record.shape = tuple(record.shape) + + # squeeze dimensions + for record in result: + record.shape, record.axes = squeeze_axes(record.shape, record.axes) + + return result + + def __len__(self): + """Return number of image pages in file.""" + return len(self.pages) + + def __getitem__(self, key): + """Return specified page.""" + return self.pages[key] + + def __iter__(self): + """Return iterator over pages.""" + return iter(self.pages) + + def __str__(self): + """Return string containing information about file.""" + result = [ + self._fh.name.capitalize(), + format_size(self._fh.size), + {'<': 'little endian', '>': 'big endian'}[self.byteorder]] + if self.is_bigtiff: + result.append("bigtiff") + if len(self.pages) > 1: + result.append("%i pages" % len(self.pages)) + if len(self.series) > 1: + result.append("%i series" % len(self.series)) + if len(self._files) > 1: + result.append("%i files" % (len(self._files))) + return ", ".join(result) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + @lazyattr + def fstat(self): + try: + return os.fstat(self._fh.fileno()) + except Exception: # io.UnsupportedOperation + return None + + @lazyattr + def is_bigtiff(self): + return self.offset_size != 4 + + @lazyattr + def is_rgb(self): + return all(p.is_rgb for p in self.pages) + + @lazyattr + def is_palette(self): + return all(p.is_palette for p in self.pages) + + @lazyattr + def is_mdgel(self): + return any(p.is_mdgel for p in self.pages) + + @lazyattr + def is_mediacy(self): + return any(p.is_mediacy for p in self.pages) + + @lazyattr + def is_stk(self): + return all(p.is_stk for p in self.pages) + + @lazyattr + def is_lsm(self): + return self.pages[0].is_lsm + + @lazyattr + def is_imagej(self): + return self.pages[0].is_imagej + + @lazyattr + def is_micromanager(self): + return self.pages[0].is_micromanager + + @lazyattr + def is_nih(self): + return self.pages[0].is_nih + + @lazyattr + def is_fluoview(self): + return self.pages[0].is_fluoview + + @lazyattr + def is_ome(self): + return self.pages[0].is_ome + + +class TiffPage(object): + """A TIFF image file directory (IFD). + + Attributes + ---------- + index : int + Index of page in file. + dtype : str {TIFF_SAMPLE_DTYPES} + Data type of image, colormapped if applicable. + shape : tuple + Dimensions of the image array in TIFF page, + colormapped and with one alpha channel if applicable. + axes : str + Axes label codes: + 'X' width, 'Y' height, 'S' sample, 'I' image series|page|plane, + 'Z' depth, 'C' color|em-wavelength|channel, 'E' ex-wavelength|lambda, + 'T' time, 'R' region|tile, 'A' angle, 'P' phase, 'H' lifetime, + 'L' exposure, 'V' event, 'Q' unknown, '_' missing + tags : TiffTags + Dictionary of tags in page. + Tag values are also directly accessible as attributes. + color_map : numpy array + Color look up table, if exists. + cz_lsm_scan_info: Record(dict) + LSM scan info attributes, if exists. + imagej_tags: Record(dict) + Consolidated ImageJ description and metadata tags, if exists. + uic_tags: Record(dict) + Consolidated MetaMorph STK/UIC tags, if exists. + + All attributes are read-only. + + Notes + ----- + The internal, normalized '_shape' attribute is 6 dimensional: + + 0. number planes (stk) + 1. planar samples_per_pixel + 2. image_depth Z (sgi) + 3. image_length Y + 4. image_width X + 5. contig samples_per_pixel + + """ + def __init__(self, parent): + """Initialize instance from file.""" + self.parent = parent + self.index = len(parent.pages) + self.shape = self._shape = () + self.dtype = self._dtype = None + self.axes = "" + self.tags = TiffTags() + + self._fromfile() + self._process_tags() + + def _fromfile(self): + """Read TIFF IFD structure and its tags from file. + + File cursor must be at storage position of IFD offset and is left at + offset to next IFD. + + Raises StopIteration if offset (first bytes read) is 0. + + """ + fh = self.parent.filehandle + byteorder = self.parent.byteorder + offset_size = self.parent.offset_size + + fmt = {4: 'I', 8: 'Q'}[offset_size] + offset = struct.unpack(byteorder + fmt, fh.read(offset_size))[0] + if not offset: + raise StopIteration() + + # read standard tags + tags = self.tags + fh.seek(offset) + fmt, size = {4: ('H', 2), 8: ('Q', 8)}[offset_size] + try: + numtags = struct.unpack(byteorder + fmt, fh.read(size))[0] + except Exception: + warnings.warn("corrupted page list") + raise StopIteration() + + tagcode = 0 + for _ in range(numtags): + try: + tag = TiffTag(self.parent) + # print(tag) + except TiffTag.Error as e: + warnings.warn(str(e)) + continue + if tagcode > tag.code: + # expected for early LSM and tifffile versions + warnings.warn("tags are not ordered by code") + tagcode = tag.code + if tag.name not in tags: + tags[tag.name] = tag + else: + # some files contain multiple IFD with same code + # e.g. MicroManager files contain two image_description + i = 1 + while True: + name = "%s_%i" % (tag.name, i) + if name not in tags: + tags[name] = tag + break + + pos = fh.tell() + + if self.is_lsm or (self.index and self.parent.is_lsm): + # correct non standard LSM bitspersample tags + self.tags['bits_per_sample']._correct_lsm_bitspersample(self) + + if self.is_lsm: + # read LSM info subrecords + for name, reader in CZ_LSM_INFO_READERS.items(): + try: + offset = self.cz_lsm_info['offset_'+name] + except KeyError: + continue + if offset < 8: + # older LSM revision + continue + fh.seek(offset) + try: + setattr(self, 'cz_lsm_'+name, reader(fh)) + except ValueError: + pass + + elif self.is_stk and 'uic1tag' in tags and not tags['uic1tag'].value: + # read uic1tag now that plane count is known + uic1tag = tags['uic1tag'] + fh.seek(uic1tag.value_offset) + tags['uic1tag'].value = Record( + read_uic1tag(fh, byteorder, uic1tag.dtype, uic1tag.count, + tags['uic2tag'].count)) + fh.seek(pos) + + def _process_tags(self): + """Validate standard tags and initialize attributes. + + Raise ValueError if tag values are not supported. + + """ + tags = self.tags + for code, (name, default, dtype, count, validate) in TIFF_TAGS.items(): + if not (name in tags or default is None): + tags[name] = TiffTag(code, dtype=dtype, count=count, + value=default, name=name) + if name in tags and validate: + try: + if tags[name].count == 1: + setattr(self, name, validate[tags[name].value]) + else: + setattr(self, name, tuple( + validate[value] for value in tags[name].value)) + except KeyError: + raise ValueError("%s.value (%s) not supported" % + (name, tags[name].value)) + + tag = tags['bits_per_sample'] + if tag.count == 1: + self.bits_per_sample = tag.value + else: + # LSM might list more items than samples_per_pixel + value = tag.value[:self.samples_per_pixel] + if any((v-value[0] for v in value)): + self.bits_per_sample = value + else: + self.bits_per_sample = value[0] + + tag = tags['sample_format'] + if tag.count == 1: + self.sample_format = TIFF_SAMPLE_FORMATS[tag.value] + else: + value = tag.value[:self.samples_per_pixel] + if any((v-value[0] for v in value)): + self.sample_format = [TIFF_SAMPLE_FORMATS[v] for v in value] + else: + self.sample_format = TIFF_SAMPLE_FORMATS[value[0]] + + if 'photometric' not in tags: + self.photometric = None + + if 'image_depth' not in tags: + self.image_depth = 1 + + if 'image_length' in tags: + self.strips_per_image = int(math.floor( + float(self.image_length + self.rows_per_strip - 1) / + self.rows_per_strip)) + else: + self.strips_per_image = 0 + + key = (self.sample_format, self.bits_per_sample) + self.dtype = self._dtype = TIFF_SAMPLE_DTYPES.get(key, None) + + if 'image_length' not in self.tags or 'image_width' not in self.tags: + # some GEL file pages are missing image data + self.image_length = 0 + self.image_width = 0 + self.image_depth = 0 + self.strip_offsets = 0 + self._shape = () + self.shape = () + self.axes = '' + + if self.is_palette: + self.dtype = self.tags['color_map'].dtype[1] + self.color_map = numpy.array(self.color_map, self.dtype) + dmax = self.color_map.max() + if dmax < 256: + self.dtype = numpy.uint8 + self.color_map = self.color_map.astype(self.dtype) + #else: + # self.dtype = numpy.uint8 + # self.color_map >>= 8 + # self.color_map = self.color_map.astype(self.dtype) + self.color_map.shape = (3, -1) + + # determine shape of data + image_length = self.image_length + image_width = self.image_width + image_depth = self.image_depth + samples_per_pixel = self.samples_per_pixel + + if self.is_stk: + assert self.image_depth == 1 + planes = self.tags['uic2tag'].count + if self.is_contig: + self._shape = (planes, 1, 1, image_length, image_width, + samples_per_pixel) + if samples_per_pixel == 1: + self.shape = (planes, image_length, image_width) + self.axes = 'YX' + else: + self.shape = (planes, image_length, image_width, + samples_per_pixel) + self.axes = 'YXS' + else: + self._shape = (planes, samples_per_pixel, 1, image_length, + image_width, 1) + if samples_per_pixel == 1: + self.shape = (planes, image_length, image_width) + self.axes = 'YX' + else: + self.shape = (planes, samples_per_pixel, image_length, + image_width) + self.axes = 'SYX' + # detect type of series + if planes == 1: + self.shape = self.shape[1:] + elif numpy.all(self.uic2tag.z_distance != 0): + self.axes = 'Z' + self.axes + elif numpy.all(numpy.diff(self.uic2tag.time_created) != 0): + self.axes = 'T' + self.axes + else: + self.axes = 'I' + self.axes + # DISABLED + if self.is_palette: + assert False, "color mapping disabled for stk" + if self.color_map.shape[1] >= 2**self.bits_per_sample: + if image_depth == 1: + self.shape = (3, planes, image_length, image_width) + else: + self.shape = (3, planes, image_depth, image_length, + image_width) + self.axes = 'C' + self.axes + else: + warnings.warn("palette cannot be applied") + self.is_palette = False + elif self.is_palette: + samples = 1 + if 'extra_samples' in self.tags: + samples += len(self.extra_samples) + if self.is_contig: + self._shape = (1, 1, image_depth, image_length, image_width, + samples) + else: + self._shape = (1, samples, image_depth, image_length, + image_width, 1) + if self.color_map.shape[1] >= 2**self.bits_per_sample: + if image_depth == 1: + self.shape = (3, image_length, image_width) + self.axes = 'CYX' + else: + self.shape = (3, image_depth, image_length, image_width) + self.axes = 'CZYX' + else: + warnings.warn("palette cannot be applied") + self.is_palette = False + if image_depth == 1: + self.shape = (image_length, image_width) + self.axes = 'YX' + else: + self.shape = (image_depth, image_length, image_width) + self.axes = 'ZYX' + elif self.is_rgb or samples_per_pixel > 1: + if self.is_contig: + self._shape = (1, 1, image_depth, image_length, image_width, + samples_per_pixel) + if image_depth == 1: + self.shape = (image_length, image_width, samples_per_pixel) + self.axes = 'YXS' + else: + self.shape = (image_depth, image_length, image_width, + samples_per_pixel) + self.axes = 'ZYXS' + else: + self._shape = (1, samples_per_pixel, image_depth, + image_length, image_width, 1) + if image_depth == 1: + self.shape = (samples_per_pixel, image_length, image_width) + self.axes = 'SYX' + else: + self.shape = (samples_per_pixel, image_depth, + image_length, image_width) + self.axes = 'SZYX' + if False and self.is_rgb and 'extra_samples' in self.tags: + # DISABLED: only use RGB and first alpha channel if exists + extra_samples = self.extra_samples + if self.tags['extra_samples'].count == 1: + extra_samples = (extra_samples, ) + for exs in extra_samples: + if exs in ('unassalpha', 'assocalpha', 'unspecified'): + if self.is_contig: + self.shape = self.shape[:-1] + (4,) + else: + self.shape = (4,) + self.shape[1:] + break + else: + self._shape = (1, 1, image_depth, image_length, image_width, 1) + if image_depth == 1: + self.shape = (image_length, image_width) + self.axes = 'YX' + else: + self.shape = (image_depth, image_length, image_width) + self.axes = 'ZYX' + if not self.compression and 'strip_byte_counts' not in tags: + self.strip_byte_counts = ( + product(self.shape) * (self.bits_per_sample // 8), ) + + assert len(self.shape) == len(self.axes) + + def asarray(self, squeeze=True, colormapped=True, rgbonly=False, + scale_mdgel=False, memmap=False, reopen=True): + """Read image data from file and return as numpy array. + + Raise ValueError if format is unsupported. + If any of 'squeeze', 'colormapped', or 'rgbonly' are not the default, + the shape of the returned array might be different from the page shape. + + Parameters + ---------- + squeeze : bool + If True, all length-1 dimensions (except X and Y) are + squeezed out from result. + colormapped : bool + If True, color mapping is applied for palette-indexed images. + rgbonly : bool + If True, return RGB(A) image without additional extra samples. + memmap : bool + If True, use numpy.memmap to read arrays from file if possible. + For use on 64 bit systems and files with few huge contiguous data. + reopen : bool + If True and the parent file handle is closed, the file is + temporarily re-opened (and closed if no exception occurs). + scale_mdgel : bool + If True, MD Gel data will be scaled according to the private + metadata in the second TIFF page. The dtype will be float32. + + """ + if not self._shape: + return + + if self.dtype is None: + raise ValueError("data type not supported: %s%i" % ( + self.sample_format, self.bits_per_sample)) + if self.compression not in TIFF_DECOMPESSORS: + raise ValueError("cannot decompress %s" % self.compression) + tag = self.tags['sample_format'] + if tag.count != 1 and any((i-tag.value[0] for i in tag.value)): + raise ValueError("sample formats don't match %s" % str(tag.value)) + + fh = self.parent.filehandle + closed = fh.closed + if closed: + if reopen: + fh.open() + else: + raise IOError("file handle is closed") + + dtype = self._dtype + shape = self._shape + image_width = self.image_width + image_length = self.image_length + image_depth = self.image_depth + typecode = self.parent.byteorder + dtype + bits_per_sample = self.bits_per_sample + + if self.is_tiled: + if 'tile_offsets' in self.tags: + byte_counts = self.tile_byte_counts + offsets = self.tile_offsets + else: + byte_counts = self.strip_byte_counts + offsets = self.strip_offsets + tile_width = self.tile_width + tile_length = self.tile_length + tile_depth = self.tile_depth if 'tile_depth' in self.tags else 1 + tw = (image_width + tile_width - 1) // tile_width + tl = (image_length + tile_length - 1) // tile_length + td = (image_depth + tile_depth - 1) // tile_depth + shape = (shape[0], shape[1], + td*tile_depth, tl*tile_length, tw*tile_width, shape[-1]) + tile_shape = (tile_depth, tile_length, tile_width, shape[-1]) + runlen = tile_width + else: + byte_counts = self.strip_byte_counts + offsets = self.strip_offsets + runlen = image_width + + if any(o < 2 for o in offsets): + raise ValueError("corrupted page") + + if memmap and self._is_memmappable(rgbonly, colormapped): + result = fh.memmap_array(typecode, shape, offset=offsets[0]) + elif self.is_contiguous: + fh.seek(offsets[0]) + result = fh.read_array(typecode, product(shape)) + result = result.astype('=' + dtype) + else: + if self.is_contig: + runlen *= self.samples_per_pixel + if bits_per_sample in (8, 16, 32, 64, 128): + if (bits_per_sample * runlen) % 8: + raise ValueError("data and sample size mismatch") + + def unpack(x): + try: + return numpy.fromstring(x, typecode) + except ValueError as e: + # strips may be missing EOI + warnings.warn("unpack: %s" % e) + xlen = ((len(x) // (bits_per_sample // 8)) + * (bits_per_sample // 8)) + return numpy.fromstring(x[:xlen], typecode) + + elif isinstance(bits_per_sample, tuple): + def unpack(x): + return unpackrgb(x, typecode, bits_per_sample) + else: + def unpack(x): + return unpackints(x, typecode, bits_per_sample, runlen) + + decompress = TIFF_DECOMPESSORS[self.compression] + if self.compression == 'jpeg': + table = self.jpeg_tables if 'jpeg_tables' in self.tags else b'' + decompress = lambda x: decodejpg(x, table, self.photometric) + + if self.is_tiled: + result = numpy.empty(shape, dtype) + tw, tl, td, pl = 0, 0, 0, 0 + for offset, bytecount in zip(offsets, byte_counts): + fh.seek(offset) + tile = unpack(decompress(fh.read(bytecount))) + tile.shape = tile_shape + if self.predictor == 'horizontal': + numpy.cumsum(tile, axis=-2, dtype=dtype, out=tile) + result[0, pl, td:td+tile_depth, + tl:tl+tile_length, tw:tw+tile_width, :] = tile + del tile + tw += tile_width + if tw >= shape[4]: + tw, tl = 0, tl + tile_length + if tl >= shape[3]: + tl, td = 0, td + tile_depth + if td >= shape[2]: + td, pl = 0, pl + 1 + result = result[..., + :image_depth, :image_length, :image_width, :] + else: + strip_size = (self.rows_per_strip * self.image_width * + self.samples_per_pixel) + result = numpy.empty(shape, dtype).reshape(-1) + index = 0 + for offset, bytecount in zip(offsets, byte_counts): + fh.seek(offset) + strip = fh.read(bytecount) + strip = decompress(strip) + strip = unpack(strip) + size = min(result.size, strip.size, strip_size, + result.size - index) + result[index:index+size] = strip[:size] + del strip + index += size + + result.shape = self._shape + + if self.predictor == 'horizontal' and not (self.is_tiled and not + self.is_contiguous): + # work around bug in LSM510 software + if not (self.parent.is_lsm and not self.compression): + numpy.cumsum(result, axis=-2, dtype=dtype, out=result) + + if colormapped and self.is_palette: + if self.color_map.shape[1] >= 2**bits_per_sample: + # FluoView and LSM might fail here + result = numpy.take(self.color_map, + result[:, 0, :, :, :, 0], axis=1) + elif rgbonly and self.is_rgb and 'extra_samples' in self.tags: + # return only RGB and first alpha channel if exists + extra_samples = self.extra_samples + if self.tags['extra_samples'].count == 1: + extra_samples = (extra_samples, ) + for i, exs in enumerate(extra_samples): + if exs in ('unassalpha', 'assocalpha', 'unspecified'): + if self.is_contig: + result = result[..., [0, 1, 2, 3+i]] + else: + result = result[:, [0, 1, 2, 3+i]] + break + else: + if self.is_contig: + result = result[..., :3] + else: + result = result[:, :3] + + if squeeze: + try: + result.shape = self.shape + except ValueError: + warnings.warn("failed to reshape from %s to %s" % ( + str(result.shape), str(self.shape))) + + if scale_mdgel and self.parent.is_mdgel: + # MD Gel stores private metadata in the second page + tags = self.parent.pages[1] + if tags.md_file_tag in (2, 128): + scale = tags.md_scale_pixel + scale = scale[0] / scale[1] # rational + result = result.astype('float32') + if tags.md_file_tag == 2: + result **= 2 # squary root data format + result *= scale + + if closed: + # TODO: file remains open if an exception occurred above + fh.close() + return result + + def _is_memmappable(self, rgbonly, colormapped): + """Return if image data in file can be memory mapped.""" + if not self.parent.filehandle.is_file or not self.is_contiguous: + return False + return not (self.predictor or + (rgbonly and 'extra_samples' in self.tags) or + (colormapped and self.is_palette) or + ({'big': '>', 'little': '<'}[sys.byteorder] != + self.parent.byteorder)) + + @lazyattr + def is_contiguous(self): + """Return offset and size of contiguous data, else None. + + Excludes prediction and colormapping. + + """ + if self.compression or self.bits_per_sample not in (8, 16, 32, 64): + return + if self.is_tiled: + if (self.image_width != self.tile_width or + self.image_length % self.tile_length or + self.tile_width % 16 or self.tile_length % 16): + return + if ('image_depth' in self.tags and 'tile_depth' in self.tags and + (self.image_length != self.tile_length or + self.image_depth % self.tile_depth)): + return + offsets = self.tile_offsets + byte_counts = self.tile_byte_counts + else: + offsets = self.strip_offsets + byte_counts = self.strip_byte_counts + if len(offsets) == 1: + return offsets[0], byte_counts[0] + if self.is_stk or all(offsets[i] + byte_counts[i] == offsets[i+1] + or byte_counts[i+1] == 0 # no data/ignore offset + for i in range(len(offsets)-1)): + return offsets[0], sum(byte_counts) + + def __str__(self): + """Return string containing information about page.""" + s = ', '.join(s for s in ( + ' x '.join(str(i) for i in self.shape), + str(numpy.dtype(self.dtype)), + '%s bit' % str(self.bits_per_sample), + self.photometric if 'photometric' in self.tags else '', + self.compression if self.compression else 'raw', + '|'.join(t[3:] for t in ( + 'is_stk', 'is_lsm', 'is_nih', 'is_ome', 'is_imagej', + 'is_micromanager', 'is_fluoview', 'is_mdgel', 'is_mediacy', + 'is_sgi', 'is_reduced', 'is_tiled', + 'is_contiguous') if getattr(self, t))) if s) + return "Page %i: %s" % (self.index, s) + + def __getattr__(self, name): + """Return tag value.""" + if name in self.tags: + value = self.tags[name].value + setattr(self, name, value) + return value + raise AttributeError(name) + + @lazyattr + def uic_tags(self): + """Consolidate UIC tags.""" + if not self.is_stk: + raise AttributeError("uic_tags") + tags = self.tags + result = Record() + result.number_planes = tags['uic2tag'].count + if 'image_description' in tags: + result.plane_descriptions = self.image_description.split(b'\x00') + if 'uic1tag' in tags: + result.update(tags['uic1tag'].value) + if 'uic3tag' in tags: + result.update(tags['uic3tag'].value) # wavelengths + if 'uic4tag' in tags: + result.update(tags['uic4tag'].value) # override uic1 tags + uic2tag = tags['uic2tag'].value + result.z_distance = uic2tag.z_distance + result.time_created = uic2tag.time_created + result.time_modified = uic2tag.time_modified + try: + result.datetime_created = [ + julian_datetime(*dt) for dt in + zip(uic2tag.date_created, uic2tag.time_created)] + result.datetime_modified = [ + julian_datetime(*dt) for dt in + zip(uic2tag.date_modified, uic2tag.time_modified)] + except ValueError as e: + warnings.warn("uic_tags: %s" % e) + return result + + @lazyattr + def imagej_tags(self): + """Consolidate ImageJ metadata.""" + if not self.is_imagej: + raise AttributeError("imagej_tags") + tags = self.tags + if 'image_description_1' in tags: + # MicroManager + result = imagej_description(tags['image_description_1'].value) + else: + result = imagej_description(tags['image_description'].value) + if 'imagej_metadata' in tags: + try: + result.update(imagej_metadata( + tags['imagej_metadata'].value, + tags['imagej_byte_counts'].value, + self.parent.byteorder)) + except Exception as e: + warnings.warn(str(e)) + return Record(result) + + @lazyattr + def is_rgb(self): + """True if page contains a RGB image.""" + return ('photometric' in self.tags and + self.tags['photometric'].value == 2) + + @lazyattr + def is_contig(self): + """True if page contains a contiguous image.""" + return ('planar_configuration' in self.tags and + self.tags['planar_configuration'].value == 1) + + @lazyattr + def is_palette(self): + """True if page contains a palette-colored image and not OME or STK.""" + try: + # turn off color mapping for OME-TIFF and STK + if self.is_stk or self.is_ome or self.parent.is_ome: + return False + except IndexError: + pass # OME-XML not found in first page + return ('photometric' in self.tags and + self.tags['photometric'].value == 3) + + @lazyattr + def is_tiled(self): + """True if page contains tiled image.""" + return 'tile_width' in self.tags + + @lazyattr + def is_reduced(self): + """True if page is a reduced image of another image.""" + return bool(self.tags['new_subfile_type'].value & 1) + + @lazyattr + def is_mdgel(self): + """True if page contains md_file_tag tag.""" + return 'md_file_tag' in self.tags + + @lazyattr + def is_mediacy(self): + """True if page contains Media Cybernetics Id tag.""" + return ('mc_id' in self.tags and + self.tags['mc_id'].value.startswith(b'MC TIFF')) + + @lazyattr + def is_stk(self): + """True if page contains UIC2Tag tag.""" + return 'uic2tag' in self.tags + + @lazyattr + def is_lsm(self): + """True if page contains LSM CZ_LSM_INFO tag.""" + return 'cz_lsm_info' in self.tags + + @lazyattr + def is_fluoview(self): + """True if page contains FluoView MM_STAMP tag.""" + return 'mm_stamp' in self.tags + + @lazyattr + def is_nih(self): + """True if page contains NIH image header.""" + return 'nih_image_header' in self.tags + + @lazyattr + def is_sgi(self): + """True if page contains SGI image and tile depth tags.""" + return 'image_depth' in self.tags and 'tile_depth' in self.tags + + @lazyattr + def is_ome(self): + """True if page contains OME-XML in image_description tag.""" + return ('image_description' in self.tags and self.tags[ + 'image_description'].value.startswith(b' parent.offset_size or code in CUSTOM_TAGS: + pos = fh.tell() + tof = {4: 'I', 8: 'Q'}[parent.offset_size] + self.value_offset = offset = struct.unpack(byteorder+tof, value)[0] + if offset < 0 or offset > parent.filehandle.size: + raise TiffTag.Error("corrupt file - invalid tag value offset") + elif offset < 4: + raise TiffTag.Error("corrupt value offset for tag %i" % code) + fh.seek(offset) + if code in CUSTOM_TAGS: + readfunc = CUSTOM_TAGS[code][1] + value = readfunc(fh, byteorder, dtype, count) + if isinstance(value, dict): # numpy.core.records.record + value = Record(value) + elif code in TIFF_TAGS or dtype[-1] == 's': + value = struct.unpack(fmt, fh.read(size)) + else: + value = read_numpy(fh, byteorder, dtype, count) + fh.seek(pos) + else: + value = struct.unpack(fmt, value[:size]) + + if code not in CUSTOM_TAGS and code not in (273, 279, 324, 325): + # scalar value if not strip/tile offsets/byte_counts + if len(value) == 1: + value = value[0] + + if (dtype.endswith('s') and isinstance(value, bytes) + and self._type != 7): + # TIFF ASCII fields can contain multiple strings, + # each terminated with a NUL + value = stripascii(value) + + self.code = code + self.name = name + self.dtype = dtype + self.count = count + self.value = value + + def _correct_lsm_bitspersample(self, parent): + """Correct LSM bitspersample tag. + + Old LSM writers may use a separate region for two 16-bit values, + although they fit into the tag value element of the tag. + + """ + if self.code == 258 and self.count == 2: + # TODO: test this. Need example file. + warnings.warn("correcting LSM bitspersample tag") + fh = parent.filehandle + tof = {4: '') + + def __str__(self): + """Return string containing information about tag.""" + return ' '.join(str(getattr(self, s)) for s in self.__slots__) + + +class TiffSequence(object): + """Sequence of image files. + + The data shape and dtype of all files must match. + + Properties + ---------- + files : list + List of file names. + shape : tuple + Shape of image sequence. + axes : str + Labels of axes in shape. + + Examples + -------- + >>> tifs = TiffSequence("test.oif.files/*.tif") + >>> tifs.shape, tifs.axes + ((2, 100), 'CT') + >>> data = tifs.asarray() + >>> data.shape + (2, 100, 256, 256) + + """ + _patterns = { + 'axes': r""" + # matches Olympus OIF and Leica TIFF series + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4})) + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))? + """} + + class ParseError(Exception): + pass + + def __init__(self, files, imread=TiffFile, pattern='axes', + *args, **kwargs): + """Initialize instance from multiple files. + + Parameters + ---------- + files : str, or sequence of str + Glob pattern or sequence of file names. + imread : function or class + Image read function or class with asarray function returning numpy + array from single file. + pattern : str + Regular expression pattern that matches axes names and sequence + indices in file names. + By default this matches Olympus OIF and Leica TIFF series. + + """ + if isinstance(files, basestring): + files = natural_sorted(glob.glob(files)) + files = list(files) + if not files: + raise ValueError("no files found") + #if not os.path.isfile(files[0]): + # raise ValueError("file not found") + self.files = files + + if hasattr(imread, 'asarray'): + # redefine imread + _imread = imread + + def imread(fname, *args, **kwargs): + with _imread(fname) as im: + return im.asarray(*args, **kwargs) + + self.imread = imread + + self.pattern = self._patterns.get(pattern, pattern) + try: + self._parse() + if not self.axes: + self.axes = 'I' + except self.ParseError: + self.axes = 'I' + self.shape = (len(files),) + self._start_index = (0,) + self._indices = tuple((i,) for i in range(len(files))) + + def __str__(self): + """Return string with information about image sequence.""" + return "\n".join([ + self.files[0], + '* files: %i' % len(self.files), + '* axes: %s' % self.axes, + '* shape: %s' % str(self.shape)]) + + def __len__(self): + return len(self.files) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def close(self): + pass + + def asarray(self, memmap=False, *args, **kwargs): + """Read image data from all files and return as single numpy array. + + If memmap is True, return an array stored in a binary file on disk. + The args and kwargs parameters are passed to the imread function. + + Raise IndexError or ValueError if image shapes don't match. + + """ + im = self.imread(self.files[0], *args, **kwargs) + shape = self.shape + im.shape + if memmap: + with tempfile.NamedTemporaryFile() as fh: + result = numpy.memmap(fh, dtype=im.dtype, shape=shape) + else: + result = numpy.zeros(shape, dtype=im.dtype) + result = result.reshape(-1, *im.shape) + for index, fname in zip(self._indices, self.files): + index = [i-j for i, j in zip(index, self._start_index)] + index = numpy.ravel_multi_index(index, self.shape) + im = self.imread(fname, *args, **kwargs) + result[index] = im + result.shape = shape + return result + + def _parse(self): + """Get axes and shape from file names.""" + if not self.pattern: + raise self.ParseError("invalid pattern") + pattern = re.compile(self.pattern, re.IGNORECASE | re.VERBOSE) + matches = pattern.findall(self.files[0]) + if not matches: + raise self.ParseError("pattern doesn't match file names") + matches = matches[-1] + if len(matches) % 2: + raise self.ParseError("pattern doesn't match axis name and index") + axes = ''.join(m for m in matches[::2] if m) + if not axes: + raise self.ParseError("pattern doesn't match file names") + + indices = [] + for fname in self.files: + matches = pattern.findall(fname)[-1] + if axes != ''.join(m for m in matches[::2] if m): + raise ValueError("axes don't match within the image sequence") + indices.append([int(m) for m in matches[1::2] if m]) + shape = tuple(numpy.max(indices, axis=0)) + start_index = tuple(numpy.min(indices, axis=0)) + shape = tuple(i-j+1 for i, j in zip(shape, start_index)) + if product(shape) != len(self.files): + warnings.warn("files are missing. Missing data are zeroed") + + self.axes = axes.upper() + self.shape = shape + self._indices = indices + self._start_index = start_index + + +class Record(dict): + """Dictionary with attribute access. + + Can also be initialized with numpy.core.records.record. + + """ + __slots__ = () + + def __init__(self, arg=None, **kwargs): + if kwargs: + arg = kwargs + elif arg is None: + arg = {} + try: + dict.__init__(self, arg) + except (TypeError, ValueError): + for i, name in enumerate(arg.dtype.names): + v = arg[i] + self[name] = v if v.dtype.char != 'S' else stripnull(v) + + def __getattr__(self, name): + return self[name] + + def __setattr__(self, name, value): + self.__setitem__(name, value) + + def __str__(self): + """Pretty print Record.""" + s = [] + lists = [] + for k in sorted(self): + try: + if k.startswith('_'): # does not work with byte + continue + except AttributeError: + pass + v = self[k] + if isinstance(v, (list, tuple)) and len(v): + if isinstance(v[0], Record): + lists.append((k, v)) + continue + elif isinstance(v[0], TiffPage): + v = [i.index for i in v if i] + s.append( + ("* %s: %s" % (k, str(v))).split("\n", 1)[0] + [:PRINT_LINE_LEN].rstrip()) + for k, v in lists: + l = [] + for i, w in enumerate(v): + l.append("* %s[%i]\n %s" % (k, i, + str(w).replace("\n", "\n "))) + s.append('\n'.join(l)) + return '\n'.join(s) + + +class TiffTags(Record): + """Dictionary of TiffTag with attribute access.""" + + def __str__(self): + """Return string with information about all tags.""" + s = [] + for tag in sorted(self.values(), key=lambda x: x.code): + typecode = "%i%s" % (tag.count * int(tag.dtype[0]), tag.dtype[1]) + line = "* %i %s (%s) %s" % ( + tag.code, tag.name, typecode, tag.as_str()) + s.append(line[:PRINT_LINE_LEN].lstrip()) + return '\n'.join(s) + + +class FileHandle(object): + """Binary file handle. + + * Handle embedded files (for CZI within CZI files). + * Allow to re-open closed files (for multi file formats such as OME-TIFF). + * Read numpy arrays and records from file like objects. + + Only binary read, seek, tell, and close are supported on embedded files. + When initialized from another file handle, do not use it unless this + FileHandle is closed. + + Attributes + ---------- + name : str + Name of the file. + path : str + Absolute path to file. + size : int + Size of file in bytes. + is_file : bool + If True, file has a filno and can be memory mapped. + + All attributes are read-only. + + """ + __slots__ = ('_fh', '_arg', '_mode', '_name', '_dir', + '_offset', '_size', '_close', 'is_file') + + def __init__(self, arg, mode='rb', name=None, offset=None, size=None): + """Initialize file handle from file name or another file handle. + + Parameters + ---------- + arg : str, File, or FileHandle + File name or open file handle. + mode : str + File open mode in case 'arg' is a file name. + name : str + Optional name of file in case 'arg' is a file handle. + offset : int + Optional start position of embedded file. By default this is + the current file position. + size : int + Optional size of embedded file. By default this is the number + of bytes from the 'offset' to the end of the file. + + """ + self._fh = None + self._arg = arg + self._mode = mode + self._name = name + self._dir = '' + self._offset = offset + self._size = size + self._close = True + self.is_file = False + self.open() + + def open(self): + """Open or re-open file.""" + if self._fh: + return # file is open + + if isinstance(self._arg, basestring): + # file name + self._arg = os.path.abspath(self._arg) + self._dir, self._name = os.path.split(self._arg) + self._fh = open(self._arg, self._mode) + self._close = True + if self._offset is None: + self._offset = 0 + elif isinstance(self._arg, FileHandle): + # FileHandle + self._fh = self._arg._fh + if self._offset is None: + self._offset = 0 + self._offset += self._arg._offset + self._close = False + if not self._name: + if self._offset: + name, ext = os.path.splitext(self._arg._name) + self._name = "%s@%i%s" % (name, self._offset, ext) + else: + self._name = self._arg._name + self._dir = self._arg._dir + else: + # open file object + self._fh = self._arg + if self._offset is None: + self._offset = self._arg.tell() + self._close = False + if not self._name: + try: + self._dir, self._name = os.path.split(self._fh.name) + except AttributeError: + self._name = "Unnamed stream" + + if self._offset: + self._fh.seek(self._offset) + + if self._size is None: + pos = self._fh.tell() + self._fh.seek(self._offset, 2) + self._size = self._fh.tell() + self._fh.seek(pos) + + try: + self._fh.fileno() + self.is_file = True + except Exception: + self.is_file = False + + def read(self, size=-1): + """Read 'size' bytes from file, or until EOF is reached.""" + if size < 0 and self._offset: + size = self._size + return self._fh.read(size) + + def memmap_array(self, dtype, shape, offset=0, mode='r', order='C'): + """Return numpy.memmap of data stored in file.""" + if not self.is_file: + raise ValueError("Can not memory map file without fileno.") + return numpy.memmap(self._fh, dtype=dtype, mode=mode, + offset=self._offset + offset, + shape=shape, order=order) + + def read_array(self, dtype, count=-1, sep=""): + """Return numpy array from file. + + Work around numpy issue #2230, "numpy.fromfile does not accept + StringIO object" https://github.com/numpy/numpy/issues/2230. + + """ + try: + return numpy.fromfile(self._fh, dtype, count, sep) + except IOError: + if count < 0: + size = self._size + else: + size = count * numpy.dtype(dtype).itemsize + data = self._fh.read(size) + return numpy.fromstring(data, dtype, count, sep) + + def read_record(self, dtype, shape=1, byteorder=None): + """Return numpy record from file.""" + try: + rec = numpy.rec.fromfile(self._fh, dtype, shape, + byteorder=byteorder) + except Exception: + dtype = numpy.dtype(dtype) + if shape is None: + shape = self._size // dtype.itemsize + size = product(sequence(shape)) * dtype.itemsize + data = self._fh.read(size) + return numpy.rec.fromstring(data, dtype, shape, + byteorder=byteorder) + return rec[0] if shape == 1 else rec + + def tell(self): + """Return file's current position.""" + return self._fh.tell() - self._offset + + def seek(self, offset, whence=0): + """Set file's current position.""" + if self._offset: + if whence == 0: + self._fh.seek(self._offset + offset, whence) + return + elif whence == 2: + self._fh.seek(self._offset + self._size + offset, 0) + return + self._fh.seek(offset, whence) + + def close(self): + """Close file.""" + if self._close and self._fh: + self._fh.close() + self._fh = None + self.is_file = False + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def __getattr__(self, name): + """Return attribute from underlying file object.""" + if self._offset: + warnings.warn( + "FileHandle: '%s' not implemented for embedded files" % name) + return getattr(self._fh, name) + + @property + def name(self): + return self._name + + @property + def dirname(self): + return self._dir + + @property + def path(self): + return os.path.join(self._dir, self._name) + + @property + def size(self): + return self._size + + @property + def closed(self): + return self._fh is None + + +def read_bytes(fh, byteorder, dtype, count): + """Read tag data from file and return as byte string.""" + dtype = 'b' if dtype[-1] == 's' else byteorder+dtype[-1] + return fh.read_array(dtype, count).tostring() + + +def read_numpy(fh, byteorder, dtype, count): + """Read tag data from file and return as numpy array.""" + dtype = 'b' if dtype[-1] == 's' else byteorder+dtype[-1] + return fh.read_array(dtype, count) + + +def read_json(fh, byteorder, dtype, count): + """Read JSON tag data from file and return as object.""" + data = fh.read(count) + try: + return json.loads(unicode(stripnull(data), 'utf-8')) + except ValueError: + warnings.warn("invalid JSON `%s`" % data) + + +def read_mm_header(fh, byteorder, dtype, count): + """Read MM_HEADER tag from file and return as numpy.rec.array.""" + return fh.read_record(MM_HEADER, byteorder=byteorder) + + +def read_mm_stamp(fh, byteorder, dtype, count): + """Read MM_STAMP tag from file and return as numpy.array.""" + return fh.read_array(byteorder+'f8', 8) + + +def read_uic1tag(fh, byteorder, dtype, count, plane_count=None): + """Read MetaMorph STK UIC1Tag from file and return as dictionary. + + Return empty dictionary if plane_count is unknown. + + """ + assert dtype in ('2I', '1I') and byteorder == '<' + result = {} + if dtype == '2I': + # pre MetaMorph 2.5 (not tested) + values = fh.read_array(' structure_size: + break + cz_lsm_info.append((name, dtype)) + else: + cz_lsm_info = CZ_LSM_INFO + + return fh.read_record(cz_lsm_info, byteorder=byteorder) + + +def read_cz_lsm_floatpairs(fh): + """Read LSM sequence of float pairs from file and return as list.""" + size = struct.unpack(' 0: + esize, etime, etype = struct.unpack(''}[fh.read(2)] + except IndexError: + raise ValueError("not a MicroManager TIFF file") + + results = {} + fh.seek(8) + (index_header, index_offset, display_header, display_offset, + comments_header, comments_offset, summary_header, summary_length + ) = struct.unpack(byteorder + "IIIIIIII", fh.read(32)) + + if summary_header != 2355492: + raise ValueError("invalid MicroManager summary_header") + results['summary'] = read_json(fh, byteorder, None, summary_length) + + if index_header != 54773648: + raise ValueError("invalid MicroManager index_header") + fh.seek(index_offset) + header, count = struct.unpack(byteorder + "II", fh.read(8)) + if header != 3453623: + raise ValueError("invalid MicroManager index_header") + data = struct.unpack(byteorder + "IIIII"*count, fh.read(20*count)) + results['index_map'] = { + 'channel': data[::5], 'slice': data[1::5], 'frame': data[2::5], + 'position': data[3::5], 'offset': data[4::5]} + + if display_header != 483765892: + raise ValueError("invalid MicroManager display_header") + fh.seek(display_offset) + header, count = struct.unpack(byteorder + "II", fh.read(8)) + if header != 347834724: + raise ValueError("invalid MicroManager display_header") + results['display_settings'] = read_json(fh, byteorder, None, count) + + if comments_header != 99384722: + raise ValueError("invalid MicroManager comments_header") + fh.seek(comments_offset) + header, count = struct.unpack(byteorder + "II", fh.read(8)) + if header != 84720485: + raise ValueError("invalid MicroManager comments_header") + results['comments'] = read_json(fh, byteorder, None, count) + + return results + + +def imagej_metadata(data, bytecounts, byteorder): + """Return dict from ImageJ metadata tag value.""" + _str = str if sys.version_info[0] < 3 else lambda x: str(x, 'cp1252') + + def read_string(data, byteorder): + return _str(stripnull(data[0 if byteorder == '<' else 1::2])) + + def read_double(data, byteorder): + return struct.unpack(byteorder+('d' * (len(data) // 8)), data) + + def read_bytes(data, byteorder): + #return struct.unpack('b' * len(data), data) + return numpy.fromstring(data, 'uint8') + + metadata_types = { # big endian + b'info': ('info', read_string), + b'labl': ('labels', read_string), + b'rang': ('ranges', read_double), + b'luts': ('luts', read_bytes), + b'roi ': ('roi', read_bytes), + b'over': ('overlays', read_bytes)} + metadata_types.update( # little endian + dict((k[::-1], v) for k, v in metadata_types.items())) + + if not bytecounts: + raise ValueError("no ImageJ metadata") + + if not data[:4] in (b'IJIJ', b'JIJI'): + raise ValueError("invalid ImageJ metadata") + + header_size = bytecounts[0] + if header_size < 12 or header_size > 804: + raise ValueError("invalid ImageJ metadata header size") + + ntypes = (header_size - 4) // 8 + header = struct.unpack(byteorder+'4sI'*ntypes, data[4:4+ntypes*8]) + pos = 4 + ntypes * 8 + counter = 0 + result = {} + for mtype, count in zip(header[::2], header[1::2]): + values = [] + name, func = metadata_types.get(mtype, (_str(mtype), read_bytes)) + for _ in range(count): + counter += 1 + pos1 = pos + bytecounts[counter] + values.append(func(data[pos:pos1], byteorder)) + pos = pos1 + result[name.strip()] = values[0] if count == 1 else values + return result + + +def imagej_description(description): + """Return dict from ImageJ image_description tag.""" + def _bool(val): + return {b'true': True, b'false': False}[val.lower()] + + _str = str if sys.version_info[0] < 3 else lambda x: str(x, 'cp1252') + result = {} + for line in description.splitlines(): + try: + key, val = line.split(b'=') + except Exception: + continue + key = key.strip() + val = val.strip() + for dtype in (int, float, _bool, _str): + try: + val = dtype(val) + break + except Exception: + pass + result[_str(key)] = val + return result + + +def _replace_by(module_function, package=None, warn=False): + """Try replace decorated function by module.function.""" + try: + from importlib import import_module + except ImportError: + warnings.warn('could not import module importlib') + return lambda func: func + + def decorate(func, module_function=module_function, warn=warn): + try: + module, function = module_function.split('.') + if not package: + module = import_module(module) + else: + module = import_module('.' + module, package=package) + func, oldfunc = getattr(module, function), func + globals()['__old_' + func.__name__] = oldfunc + except Exception: + if warn: + warnings.warn("failed to import %s" % module_function) + return func + + return decorate + + +def decodejpg(encoded, tables=b'', photometric=None, + ycbcr_subsampling=None, ycbcr_positioning=None): + """Decode JPEG encoded byte string (using _czifile extension module).""" + import _czifile + image = _czifile.decodejpg(encoded, tables) + if photometric == 'rgb' and ycbcr_subsampling and ycbcr_positioning: + # TODO: convert YCbCr to RGB + pass + return image.tostring() + + +@_replace_by('_tifffile.decodepackbits') +def decodepackbits(encoded): + """Decompress PackBits encoded byte string. + + PackBits is a simple byte-oriented run-length compression scheme. + + """ + func = ord if sys.version[0] == '2' else lambda x: x + result = [] + result_extend = result.extend + i = 0 + try: + while True: + n = func(encoded[i]) + 1 + i += 1 + if n < 129: + result_extend(encoded[i:i+n]) + i += n + elif n > 129: + result_extend(encoded[i:i+1] * (258-n)) + i += 1 + except IndexError: + pass + return b''.join(result) if sys.version[0] == '2' else bytes(result) + + +@_replace_by('_tifffile.decodelzw') +def decodelzw(encoded): + """Decompress LZW (Lempel-Ziv-Welch) encoded TIFF strip (byte string). + + The strip must begin with a CLEAR code and end with an EOI code. + + This is an implementation of the LZW decoding algorithm described in (1). + It is not compatible with old style LZW compressed files like quad-lzw.tif. + + """ + len_encoded = len(encoded) + bitcount_max = len_encoded * 8 + unpack = struct.unpack + + if sys.version[0] == '2': + newtable = [chr(i) for i in range(256)] + else: + newtable = [bytes([i]) for i in range(256)] + newtable.extend((0, 0)) + + def next_code(): + """Return integer of `bitw` bits at `bitcount` position in encoded.""" + start = bitcount // 8 + s = encoded[start:start+4] + try: + code = unpack('>I', s)[0] + except Exception: + code = unpack('>I', s + b'\x00'*(4-len(s)))[0] + code <<= bitcount % 8 + code &= mask + return code >> shr + + switchbitch = { # code: bit-width, shr-bits, bit-mask + 255: (9, 23, int(9*'1'+'0'*23, 2)), + 511: (10, 22, int(10*'1'+'0'*22, 2)), + 1023: (11, 21, int(11*'1'+'0'*21, 2)), + 2047: (12, 20, int(12*'1'+'0'*20, 2)), } + bitw, shr, mask = switchbitch[255] + bitcount = 0 + + if len_encoded < 4: + raise ValueError("strip must be at least 4 characters long") + + if next_code() != 256: + raise ValueError("strip must begin with CLEAR code") + + code = 0 + oldcode = 0 + result = [] + result_append = result.append + while True: + code = next_code() # ~5% faster when inlining this function + bitcount += bitw + if code == 257 or bitcount >= bitcount_max: # EOI + break + if code == 256: # CLEAR + table = newtable[:] + table_append = table.append + lentable = 258 + bitw, shr, mask = switchbitch[255] + code = next_code() + bitcount += bitw + if code == 257: # EOI + break + result_append(table[code]) + else: + if code < lentable: + decoded = table[code] + newcode = table[oldcode] + decoded[:1] + else: + newcode = table[oldcode] + newcode += newcode[:1] + decoded = newcode + result_append(decoded) + table_append(newcode) + lentable += 1 + oldcode = code + if lentable in switchbitch: + bitw, shr, mask = switchbitch[lentable] + + if code != 257: + warnings.warn("unexpected end of lzw stream (code %i)" % code) + + return b''.join(result) + + +@_replace_by('_tifffile.unpackints') +def unpackints(data, dtype, itemsize, runlen=0): + """Decompress byte string to array of integers of any bit size <= 32. + + Parameters + ---------- + data : byte str + Data to decompress. + dtype : numpy.dtype or str + A numpy boolean or integer type. + itemsize : int + Number of bits per integer. + runlen : int + Number of consecutive integers, after which to start at next byte. + + """ + if itemsize == 1: # bitarray + data = numpy.fromstring(data, '|B') + data = numpy.unpackbits(data) + if runlen % 8: + data = data.reshape(-1, runlen + (8 - runlen % 8)) + data = data[:, :runlen].reshape(-1) + return data.astype(dtype) + + dtype = numpy.dtype(dtype) + if itemsize in (8, 16, 32, 64): + return numpy.fromstring(data, dtype) + if itemsize < 1 or itemsize > 32: + raise ValueError("itemsize out of range: %i" % itemsize) + if dtype.kind not in "biu": + raise ValueError("invalid dtype") + + itembytes = next(i for i in (1, 2, 4, 8) if 8 * i >= itemsize) + if itembytes != dtype.itemsize: + raise ValueError("dtype.itemsize too small") + if runlen == 0: + runlen = len(data) // itembytes + skipbits = runlen*itemsize % 8 + if skipbits: + skipbits = 8 - skipbits + shrbits = itembytes*8 - itemsize + bitmask = int(itemsize*'1'+'0'*shrbits, 2) + dtypestr = '>' + dtype.char # dtype always big endian? + + unpack = struct.unpack + l = runlen * (len(data)*8 // (runlen*itemsize + skipbits)) + result = numpy.empty((l, ), dtype) + bitcount = 0 + for i in range(len(result)): + start = bitcount // 8 + s = data[start:start+itembytes] + try: + code = unpack(dtypestr, s)[0] + except Exception: + code = unpack(dtypestr, s + b'\x00'*(itembytes-len(s)))[0] + code <<= bitcount % 8 + code &= bitmask + result[i] = code >> shrbits + bitcount += itemsize + if (i+1) % runlen == 0: + bitcount += skipbits + return result + + +def unpackrgb(data, dtype='>> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff) + >>> print(unpackrgb(data, '>> print(unpackrgb(data, '>> print(unpackrgb(data, '= bits) + data = numpy.fromstring(data, dtype.byteorder+dt) + result = numpy.empty((data.size, len(bitspersample)), dtype.char) + for i, bps in enumerate(bitspersample): + t = data >> int(numpy.sum(bitspersample[i+1:])) + t &= int('0b'+'1'*bps, 2) + if rescale: + o = ((dtype.itemsize * 8) // bps + 1) * bps + if o > data.dtype.itemsize * 8: + t = t.astype('I') + t *= (2**o - 1) // (2**bps - 1) + t //= 2**(o - (dtype.itemsize * 8)) + result[:, i] = t + return result.reshape(-1) + + +def reorient(image, orientation): + """Return reoriented view of image array. + + Parameters + ---------- + image : numpy array + Non-squeezed output of asarray() functions. + Axes -3 and -2 must be image length and width respectively. + orientation : int or str + One of TIFF_ORIENTATIONS keys or values. + + """ + o = TIFF_ORIENTATIONS.get(orientation, orientation) + if o == 'top_left': + return image + elif o == 'top_right': + return image[..., ::-1, :] + elif o == 'bottom_left': + return image[..., ::-1, :, :] + elif o == 'bottom_right': + return image[..., ::-1, ::-1, :] + elif o == 'left_top': + return numpy.swapaxes(image, -3, -2) + elif o == 'right_top': + return numpy.swapaxes(image, -3, -2)[..., ::-1, :] + elif o == 'left_bottom': + return numpy.swapaxes(image, -3, -2)[..., ::-1, :, :] + elif o == 'right_bottom': + return numpy.swapaxes(image, -3, -2)[..., ::-1, ::-1, :] + + +def squeeze_axes(shape, axes, skip='XY'): + """Return shape and axes with single-dimensional entries removed. + + Remove unused dimensions unless their axes are listed in 'skip'. + + >>> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC') + ((5, 2, 1), 'TYX') + + """ + if len(shape) != len(axes): + raise ValueError("dimensions of axes and shape don't match") + shape, axes = zip(*(i for i in zip(shape, axes) + if i[0] > 1 or i[1] in skip)) + return shape, ''.join(axes) + + +def transpose_axes(data, axes, asaxes='CTZYX'): + """Return data with its axes permuted to match specified axes. + + A view is returned if possible. + + >>> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape + (5, 2, 1, 3, 4) + + """ + for ax in axes: + if ax not in asaxes: + raise ValueError("unknown axis %s" % ax) + # add missing axes to data + shape = data.shape + for ax in reversed(asaxes): + if ax not in axes: + axes = ax + axes + shape = (1,) + shape + data = data.reshape(shape) + # transpose axes + data = data.transpose([axes.index(ax) for ax in asaxes]) + return data + + +def stack_pages(pages, memmap=False, *args, **kwargs): + """Read data from sequence of TiffPage and stack them vertically. + + If memmap is True, return an array stored in a binary file on disk. + Additional parameters are passsed to the page asarray function. + + """ + if len(pages) == 0: + raise ValueError("no pages") + + if len(pages) == 1: + return pages[0].asarray(memmap=memmap, *args, **kwargs) + + result = pages[0].asarray(*args, **kwargs) + shape = (len(pages),) + result.shape + if memmap: + with tempfile.NamedTemporaryFile() as fh: + result = numpy.memmap(fh, dtype=result.dtype, shape=shape) + else: + result = numpy.empty(shape, dtype=result.dtype) + + for i, page in enumerate(pages): + result[i] = page.asarray(*args, **kwargs) + + return result + + +def stripnull(string): + """Return string truncated at first null character. + + Clean NULL terminated C strings. + + >>> stripnull(b'string\\x00') + b'string' + + """ + i = string.find(b'\x00') + return string if (i < 0) else string[:i] + + +def stripascii(string): + """Return string truncated at last byte that is 7bit ASCII. + + Clean NULL separated and terminated TIFF strings. + + >>> stripascii(b'string\\x00string\\n\\x01\\x00') + b'string\\x00string\\n' + >>> stripascii(b'\\x00') + b'' + + """ + # TODO: pythonize this + ord_ = ord if sys.version_info[0] < 3 else lambda x: x + i = len(string) + while i: + i -= 1 + if 8 < ord_(string[i]) < 127: + break + else: + i = -1 + return string[:i+1] + + +def format_size(size): + """Return file size as string from byte size.""" + for unit in ('B', 'KB', 'MB', 'GB', 'TB'): + if size < 2048: + return "%.f %s" % (size, unit) + size /= 1024.0 + + +def sequence(value): + """Return tuple containing value if value is not a sequence. + + >>> sequence(1) + (1,) + >>> sequence([1]) + [1] + + """ + try: + len(value) + return value + except TypeError: + return (value, ) + + +def product(iterable): + """Return product of sequence of numbers. + + Equivalent of functools.reduce(operator.mul, iterable, 1). + + >>> product([2**8, 2**30]) + 274877906944 + >>> product([]) + 1 + + """ + prod = 1 + for i in iterable: + prod *= i + return prod + + +def natural_sorted(iterable): + """Return human sorted list of strings. + + E.g. for sorting file names. + + >>> natural_sorted(['f1', 'f2', 'f10']) + ['f1', 'f2', 'f10'] + + """ + def sortkey(x): + return [(int(c) if c.isdigit() else c) for c in re.split(numbers, x)] + numbers = re.compile(r'(\d+)') + return sorted(iterable, key=sortkey) + + +def excel_datetime(timestamp, epoch=datetime.datetime.fromordinal(693594)): + """Return datetime object from timestamp in Excel serial format. + + Convert LSM time stamps. + + >>> excel_datetime(40237.029999999795) + datetime.datetime(2010, 2, 28, 0, 43, 11, 999982) + + """ + return epoch + datetime.timedelta(timestamp) + + +def julian_datetime(julianday, milisecond=0): + """Return datetime from days since 1/1/4713 BC and ms since midnight. + + Convert Julian dates according to MetaMorph. + + >>> julian_datetime(2451576, 54362783) + datetime.datetime(2000, 2, 2, 15, 6, 2, 783) + + """ + if julianday <= 1721423: + # no datetime before year 1 + return None + + a = julianday + 1 + if a > 2299160: + alpha = math.trunc((a - 1867216.25) / 36524.25) + a += 1 + alpha - alpha // 4 + b = a + (1524 if a > 1721423 else 1158) + c = math.trunc((b - 122.1) / 365.25) + d = math.trunc(365.25 * c) + e = math.trunc((b - d) / 30.6001) + + day = b - d - math.trunc(30.6001 * e) + month = e - (1 if e < 13.5 else 13) + year = c - (4716 if month > 2.5 else 4715) + + hour, milisecond = divmod(milisecond, 1000 * 60 * 60) + minute, milisecond = divmod(milisecond, 1000 * 60) + second, milisecond = divmod(milisecond, 1000) + + return datetime.datetime(year, month, day, + hour, minute, second, milisecond) + + +def test_tifffile(directory='testimages', verbose=True): + """Read all images in directory. + + Print error message on failure. + + >>> test_tifffile(verbose=False) + + """ + successful = 0 + failed = 0 + start = time.time() + for f in glob.glob(os.path.join(directory, '*.*')): + if verbose: + print("\n%s>\n" % f.lower(), end='') + t0 = time.time() + try: + tif = TiffFile(f, multifile=True) + except Exception as e: + if not verbose: + print(f, end=' ') + print("ERROR:", e) + failed += 1 + continue + try: + img = tif.asarray() + except ValueError: + try: + img = tif[0].asarray() + except Exception as e: + if not verbose: + print(f, end=' ') + print("ERROR:", e) + failed += 1 + continue + finally: + tif.close() + successful += 1 + if verbose: + print("%s, %s %s, %s, %.0f ms" % ( + str(tif), str(img.shape), img.dtype, tif[0].compression, + (time.time()-t0) * 1e3)) + if verbose: + print("\nSuccessfully read %i of %i files in %.3f s\n" % ( + successful, successful+failed, time.time()-start)) + + +class TIFF_SUBFILE_TYPES(object): + def __getitem__(self, key): + result = [] + if key & 1: + result.append('reduced_image') + if key & 2: + result.append('page') + if key & 4: + result.append('mask') + return tuple(result) + + +TIFF_PHOTOMETRICS = { + 0: 'miniswhite', + 1: 'minisblack', + 2: 'rgb', + 3: 'palette', + 4: 'mask', + 5: 'separated', # CMYK + 6: 'ycbcr', + 8: 'cielab', + 9: 'icclab', + 10: 'itulab', + 32803: 'cfa', # Color Filter Array + 32844: 'logl', + 32845: 'logluv', + 34892: 'linear_raw' +} + +TIFF_COMPESSIONS = { + 1: None, + 2: 'ccittrle', + 3: 'ccittfax3', + 4: 'ccittfax4', + 5: 'lzw', + 6: 'ojpeg', + 7: 'jpeg', + 8: 'adobe_deflate', + 9: 't85', + 10: 't43', + 32766: 'next', + 32771: 'ccittrlew', + 32773: 'packbits', + 32809: 'thunderscan', + 32895: 'it8ctpad', + 32896: 'it8lw', + 32897: 'it8mp', + 32898: 'it8bl', + 32908: 'pixarfilm', + 32909: 'pixarlog', + 32946: 'deflate', + 32947: 'dcs', + 34661: 'jbig', + 34676: 'sgilog', + 34677: 'sgilog24', + 34712: 'jp2000', + 34713: 'nef', +} + +TIFF_DECOMPESSORS = { + None: lambda x: x, + 'adobe_deflate': zlib.decompress, + 'deflate': zlib.decompress, + 'packbits': decodepackbits, + 'lzw': decodelzw, + # 'jpeg': decodejpg +} + +TIFF_DATA_TYPES = { + 1: '1B', # BYTE 8-bit unsigned integer. + 2: '1s', # ASCII 8-bit byte that contains a 7-bit ASCII code; + # the last byte must be NULL (binary zero). + 3: '1H', # SHORT 16-bit (2-byte) unsigned integer + 4: '1I', # LONG 32-bit (4-byte) unsigned integer. + 5: '2I', # RATIONAL Two LONGs: the first represents the numerator of + # a fraction; the second, the denominator. + 6: '1b', # SBYTE An 8-bit signed (twos-complement) integer. + 7: '1s', # UNDEFINED An 8-bit byte that may contain anything, + # depending on the definition of the field. + 8: '1h', # SSHORT A 16-bit (2-byte) signed (twos-complement) integer. + 9: '1i', # SLONG A 32-bit (4-byte) signed (twos-complement) integer. + 10: '2i', # SRATIONAL Two SLONGs: the first represents the numerator + # of a fraction, the second the denominator. + 11: '1f', # FLOAT Single precision (4-byte) IEEE format. + 12: '1d', # DOUBLE Double precision (8-byte) IEEE format. + 13: '1I', # IFD unsigned 4 byte IFD offset. + #14: '', # UNICODE + #15: '', # COMPLEX + 16: '1Q', # LONG8 unsigned 8 byte integer (BigTiff) + 17: '1q', # SLONG8 signed 8 byte integer (BigTiff) + 18: '1Q', # IFD8 unsigned 8 byte IFD offset (BigTiff) +} + +TIFF_SAMPLE_FORMATS = { + 1: 'uint', + 2: 'int', + 3: 'float', + #4: 'void', + #5: 'complex_int', + 6: 'complex', +} + +TIFF_SAMPLE_DTYPES = { + ('uint', 1): '?', # bitmap + ('uint', 2): 'B', + ('uint', 3): 'B', + ('uint', 4): 'B', + ('uint', 5): 'B', + ('uint', 6): 'B', + ('uint', 7): 'B', + ('uint', 8): 'B', + ('uint', 9): 'H', + ('uint', 10): 'H', + ('uint', 11): 'H', + ('uint', 12): 'H', + ('uint', 13): 'H', + ('uint', 14): 'H', + ('uint', 15): 'H', + ('uint', 16): 'H', + ('uint', 17): 'I', + ('uint', 18): 'I', + ('uint', 19): 'I', + ('uint', 20): 'I', + ('uint', 21): 'I', + ('uint', 22): 'I', + ('uint', 23): 'I', + ('uint', 24): 'I', + ('uint', 25): 'I', + ('uint', 26): 'I', + ('uint', 27): 'I', + ('uint', 28): 'I', + ('uint', 29): 'I', + ('uint', 30): 'I', + ('uint', 31): 'I', + ('uint', 32): 'I', + ('uint', 64): 'Q', + ('int', 8): 'b', + ('int', 16): 'h', + ('int', 32): 'i', + ('int', 64): 'q', + ('float', 16): 'e', + ('float', 32): 'f', + ('float', 64): 'd', + ('complex', 64): 'F', + ('complex', 128): 'D', + ('uint', (5, 6, 5)): 'B', +} + +TIFF_ORIENTATIONS = { + 1: 'top_left', + 2: 'top_right', + 3: 'bottom_right', + 4: 'bottom_left', + 5: 'left_top', + 6: 'right_top', + 7: 'right_bottom', + 8: 'left_bottom', +} + +# TODO: is there a standard for character axes labels? +AXES_LABELS = { + 'X': 'width', + 'Y': 'height', + 'Z': 'depth', + 'S': 'sample', # rgb(a) + 'I': 'series', # general sequence, plane, page, IFD + 'T': 'time', + 'C': 'channel', # color, emission wavelength + 'A': 'angle', + 'P': 'phase', # formerly F # P is Position in LSM! + 'R': 'tile', # region, point, mosaic + 'H': 'lifetime', # histogram + 'E': 'lambda', # excitation wavelength + 'L': 'exposure', # lux + 'V': 'event', + 'Q': 'other', + #'M': 'mosaic', # LSM 6 +} + +AXES_LABELS.update(dict((v, k) for k, v in AXES_LABELS.items())) + +# Map OME pixel types to numpy dtype +OME_PIXEL_TYPES = { + 'int8': 'i1', + 'int16': 'i2', + 'int32': 'i4', + 'uint8': 'u1', + 'uint16': 'u2', + 'uint32': 'u4', + 'float': 'f4', + # 'bit': 'bit', + 'double': 'f8', + 'complex': 'c8', + 'double-complex': 'c16', +} + +# NIH Image PicHeader v1.63 +NIH_IMAGE_HEADER = [ + ('fileid', 'a8'), + ('nlines', 'i2'), + ('pixelsperline', 'i2'), + ('version', 'i2'), + ('oldlutmode', 'i2'), + ('oldncolors', 'i2'), + ('colors', 'u1', (3, 32)), + ('oldcolorstart', 'i2'), + ('colorwidth', 'i2'), + ('extracolors', 'u2', (6, 3)), + ('nextracolors', 'i2'), + ('foregroundindex', 'i2'), + ('backgroundindex', 'i2'), + ('xscale', 'f8'), + ('_x0', 'i2'), + ('_x1', 'i2'), + ('units_t', 'i2'), # NIH_UNITS_TYPE + ('p1', [('x', 'i2'), ('y', 'i2')]), + ('p2', [('x', 'i2'), ('y', 'i2')]), + ('curvefit_t', 'i2'), # NIH_CURVEFIT_TYPE + ('ncoefficients', 'i2'), + ('coeff', 'f8', 6), + ('_um_len', 'u1'), + ('um', 'a15'), + ('_x2', 'u1'), + ('binarypic', 'b1'), + ('slicestart', 'i2'), + ('sliceend', 'i2'), + ('scalemagnification', 'f4'), + ('nslices', 'i2'), + ('slicespacing', 'f4'), + ('currentslice', 'i2'), + ('frameinterval', 'f4'), + ('pixelaspectratio', 'f4'), + ('colorstart', 'i2'), + ('colorend', 'i2'), + ('ncolors', 'i2'), + ('fill1', '3u2'), + ('fill2', '3u2'), + ('colortable_t', 'u1'), # NIH_COLORTABLE_TYPE + ('lutmode_t', 'u1'), # NIH_LUTMODE_TYPE + ('invertedtable', 'b1'), + ('zeroclip', 'b1'), + ('_xunit_len', 'u1'), + ('xunit', 'a11'), + ('stacktype_t', 'i2'), # NIH_STACKTYPE_TYPE +] + +NIH_COLORTABLE_TYPE = ( + 'CustomTable', 'AppleDefault', 'Pseudo20', 'Pseudo32', 'Rainbow', + 'Fire1', 'Fire2', 'Ice', 'Grays', 'Spectrum') + +NIH_LUTMODE_TYPE = ( + 'PseudoColor', 'OldAppleDefault', 'OldSpectrum', 'GrayScale', + 'ColorLut', 'CustomGrayscale') + +NIH_CURVEFIT_TYPE = ( + 'StraightLine', 'Poly2', 'Poly3', 'Poly4', 'Poly5', 'ExpoFit', + 'PowerFit', 'LogFit', 'RodbardFit', 'SpareFit1', 'Uncalibrated', + 'UncalibratedOD') + +NIH_UNITS_TYPE = ( + 'Nanometers', 'Micrometers', 'Millimeters', 'Centimeters', 'Meters', + 'Kilometers', 'Inches', 'Feet', 'Miles', 'Pixels', 'OtherUnits') + +NIH_STACKTYPE_TYPE = ( + 'VolumeStack', 'RGBStack', 'MovieStack', 'HSVStack') + +# Map Universal Imaging Corporation MetaMorph internal tag ids to name and type +UIC_TAGS = { + 0: ('auto_scale', int), + 1: ('min_scale', int), + 2: ('max_scale', int), + 3: ('spatial_calibration', int), + 4: ('x_calibration', Fraction), + 5: ('y_calibration', Fraction), + 6: ('calibration_units', str), + 7: ('name', str), + 8: ('thresh_state', int), + 9: ('thresh_state_red', int), + 10: ('tagid_10', None), # undefined + 11: ('thresh_state_green', int), + 12: ('thresh_state_blue', int), + 13: ('thresh_state_lo', int), + 14: ('thresh_state_hi', int), + 15: ('zoom', int), + 16: ('create_time', julian_datetime), + 17: ('last_saved_time', julian_datetime), + 18: ('current_buffer', int), + 19: ('gray_fit', None), + 20: ('gray_point_count', None), + 21: ('gray_x', Fraction), + 22: ('gray_y', Fraction), + 23: ('gray_min', Fraction), + 24: ('gray_max', Fraction), + 25: ('gray_unit_name', str), + 26: ('standard_lut', int), + 27: ('wavelength', int), + 28: ('stage_position', '(%i,2,2)u4'), # N xy positions as fractions + 29: ('camera_chip_offset', '(%i,2,2)u4'), # N xy offsets as fractions + 30: ('overlay_mask', None), + 31: ('overlay_compress', None), + 32: ('overlay', None), + 33: ('special_overlay_mask', None), + 34: ('special_overlay_compress', None), + 35: ('special_overlay', None), + 36: ('image_property', read_uic_image_property), + 37: ('stage_label', '%ip'), # N str + 38: ('autoscale_lo_info', Fraction), + 39: ('autoscale_hi_info', Fraction), + 40: ('absolute_z', '(%i,2)u4'), # N fractions + 41: ('absolute_z_valid', '(%i,)u4'), # N long + 42: ('gamma', int), + 43: ('gamma_red', int), + 44: ('gamma_green', int), + 45: ('gamma_blue', int), + 46: ('camera_bin', int), + 47: ('new_lut', int), + 48: ('image_property_ex', None), + 49: ('plane_property', int), + 50: ('user_lut_table', '(256,3)u1'), + 51: ('red_autoscale_info', int), + 52: ('red_autoscale_lo_info', Fraction), + 53: ('red_autoscale_hi_info', Fraction), + 54: ('red_minscale_info', int), + 55: ('red_maxscale_info', int), + 56: ('green_autoscale_info', int), + 57: ('green_autoscale_lo_info', Fraction), + 58: ('green_autoscale_hi_info', Fraction), + 59: ('green_minscale_info', int), + 60: ('green_maxscale_info', int), + 61: ('blue_autoscale_info', int), + 62: ('blue_autoscale_lo_info', Fraction), + 63: ('blue_autoscale_hi_info', Fraction), + 64: ('blue_min_scale_info', int), + 65: ('blue_max_scale_info', int), + #66: ('overlay_plane_color', read_uic_overlay_plane_color), +} + + +# Olympus FluoView +MM_DIMENSION = [ + ('name', 'a16'), + ('size', 'i4'), + ('origin', 'f8'), + ('resolution', 'f8'), + ('unit', 'a64'), +] + +MM_HEADER = [ + ('header_flag', 'i2'), + ('image_type', 'u1'), + ('image_name', 'a257'), + ('offset_data', 'u4'), + ('palette_size', 'i4'), + ('offset_palette0', 'u4'), + ('offset_palette1', 'u4'), + ('comment_size', 'i4'), + ('offset_comment', 'u4'), + ('dimensions', MM_DIMENSION, 10), + ('offset_position', 'u4'), + ('map_type', 'i2'), + ('map_min', 'f8'), + ('map_max', 'f8'), + ('min_value', 'f8'), + ('max_value', 'f8'), + ('offset_map', 'u4'), + ('gamma', 'f8'), + ('offset', 'f8'), + ('gray_channel', MM_DIMENSION), + ('offset_thumbnail', 'u4'), + ('voice_field', 'i4'), + ('offset_voice_field', 'u4'), +] + +# Carl Zeiss LSM +CZ_LSM_INFO = [ + ('magic_number', 'u4'), + ('structure_size', 'i4'), + ('dimension_x', 'i4'), + ('dimension_y', 'i4'), + ('dimension_z', 'i4'), + ('dimension_channels', 'i4'), + ('dimension_time', 'i4'), + ('data_type', 'i4'), # CZ_DATA_TYPES + ('thumbnail_x', 'i4'), + ('thumbnail_y', 'i4'), + ('voxel_size_x', 'f8'), + ('voxel_size_y', 'f8'), + ('voxel_size_z', 'f8'), + ('origin_x', 'f8'), + ('origin_y', 'f8'), + ('origin_z', 'f8'), + ('scan_type', 'u2'), + ('spectral_scan', 'u2'), + ('type_of_data', 'u4'), # CZ_TYPE_OF_DATA + ('offset_vector_overlay', 'u4'), + ('offset_input_lut', 'u4'), + ('offset_output_lut', 'u4'), + ('offset_channel_colors', 'u4'), + ('time_interval', 'f8'), + ('offset_channel_data_types', 'u4'), + ('offset_scan_info', 'u4'), # CZ_LSM_SCAN_INFO + ('offset_ks_data', 'u4'), + ('offset_time_stamps', 'u4'), + ('offset_event_list', 'u4'), + ('offset_roi', 'u4'), + ('offset_bleach_roi', 'u4'), + ('offset_next_recording', 'u4'), + # LSM 2.0 ends here + ('display_aspect_x', 'f8'), + ('display_aspect_y', 'f8'), + ('display_aspect_z', 'f8'), + ('display_aspect_time', 'f8'), + ('offset_mean_of_roi_overlay', 'u4'), + ('offset_topo_isoline_overlay', 'u4'), + ('offset_topo_profile_overlay', 'u4'), + ('offset_linescan_overlay', 'u4'), + ('offset_toolbar_flags', 'u4'), + ('offset_channel_wavelength', 'u4'), + ('offset_channel_factors', 'u4'), + ('objective_sphere_correction', 'f8'), + ('offset_unmix_parameters', 'u4'), + # LSM 3.2, 4.0 end here + ('offset_acquisition_parameters', 'u4'), + ('offset_characteristics', 'u4'), + ('offset_palette', 'u4'), + ('time_difference_x', 'f8'), + ('time_difference_y', 'f8'), + ('time_difference_z', 'f8'), + ('internal_use_1', 'u4'), + ('dimension_p', 'i4'), + ('dimension_m', 'i4'), + ('dimensions_reserved', '16i4'), + ('offset_tile_positions', 'u4'), + ('reserved_1', '9u4'), + ('offset_positions', 'u4'), + ('reserved_2', '21u4'), # must be 0 +] + +# Import functions for LSM_INFO sub-records +CZ_LSM_INFO_READERS = { + 'scan_info': read_cz_lsm_scan_info, + 'time_stamps': read_cz_lsm_time_stamps, + 'event_list': read_cz_lsm_event_list, + 'channel_colors': read_cz_lsm_floatpairs, + 'positions': read_cz_lsm_floatpairs, + 'tile_positions': read_cz_lsm_floatpairs, +} + +# Map cz_lsm_info.scan_type to dimension order +CZ_SCAN_TYPES = { + 0: 'XYZCT', # x-y-z scan + 1: 'XYZCT', # z scan (x-z plane) + 2: 'XYZCT', # line scan + 3: 'XYTCZ', # time series x-y + 4: 'XYZTC', # time series x-z + 5: 'XYTCZ', # time series 'Mean of ROIs' + 6: 'XYZTC', # time series x-y-z + 7: 'XYCTZ', # spline scan + 8: 'XYCZT', # spline scan x-z + 9: 'XYTCZ', # time series spline plane x-z + 10: 'XYZCT', # point mode +} + +# Map dimension codes to cz_lsm_info attribute +CZ_DIMENSIONS = { + 'X': 'dimension_x', + 'Y': 'dimension_y', + 'Z': 'dimension_z', + 'C': 'dimension_channels', + 'T': 'dimension_time', +} + +# Description of cz_lsm_info.data_type +CZ_DATA_TYPES = { + 0: 'varying data types', + 1: '8 bit unsigned integer', + 2: '12 bit unsigned integer', + 5: '32 bit float', +} + +# Description of cz_lsm_info.type_of_data +CZ_TYPE_OF_DATA = { + 0: 'Original scan data', + 1: 'Calculated data', + 2: '3D reconstruction', + 3: 'Topography height map', +} + +CZ_LSM_SCAN_INFO_ARRAYS = { + 0x20000000: "tracks", + 0x30000000: "lasers", + 0x60000000: "detection_channels", + 0x80000000: "illumination_channels", + 0xa0000000: "beam_splitters", + 0xc0000000: "data_channels", + 0x11000000: "timers", + 0x13000000: "markers", +} + +CZ_LSM_SCAN_INFO_STRUCTS = { + # 0x10000000: "recording", + 0x40000000: "track", + 0x50000000: "laser", + 0x70000000: "detection_channel", + 0x90000000: "illumination_channel", + 0xb0000000: "beam_splitter", + 0xd0000000: "data_channel", + 0x12000000: "timer", + 0x14000000: "marker", +} + +CZ_LSM_SCAN_INFO_ATTRIBUTES = { + # recording + 0x10000001: "name", + 0x10000002: "description", + 0x10000003: "notes", + 0x10000004: "objective", + 0x10000005: "processing_summary", + 0x10000006: "special_scan_mode", + 0x10000007: "scan_type", + 0x10000008: "scan_mode", + 0x10000009: "number_of_stacks", + 0x1000000a: "lines_per_plane", + 0x1000000b: "samples_per_line", + 0x1000000c: "planes_per_volume", + 0x1000000d: "images_width", + 0x1000000e: "images_height", + 0x1000000f: "images_number_planes", + 0x10000010: "images_number_stacks", + 0x10000011: "images_number_channels", + 0x10000012: "linscan_xy_size", + 0x10000013: "scan_direction", + 0x10000014: "time_series", + 0x10000015: "original_scan_data", + 0x10000016: "zoom_x", + 0x10000017: "zoom_y", + 0x10000018: "zoom_z", + 0x10000019: "sample_0x", + 0x1000001a: "sample_0y", + 0x1000001b: "sample_0z", + 0x1000001c: "sample_spacing", + 0x1000001d: "line_spacing", + 0x1000001e: "plane_spacing", + 0x1000001f: "plane_width", + 0x10000020: "plane_height", + 0x10000021: "volume_depth", + 0x10000023: "nutation", + 0x10000034: "rotation", + 0x10000035: "precession", + 0x10000036: "sample_0time", + 0x10000037: "start_scan_trigger_in", + 0x10000038: "start_scan_trigger_out", + 0x10000039: "start_scan_event", + 0x10000040: "start_scan_time", + 0x10000041: "stop_scan_trigger_in", + 0x10000042: "stop_scan_trigger_out", + 0x10000043: "stop_scan_event", + 0x10000044: "stop_scan_time", + 0x10000045: "use_rois", + 0x10000046: "use_reduced_memory_rois", + 0x10000047: "user", + 0x10000048: "use_bc_correction", + 0x10000049: "position_bc_correction1", + 0x10000050: "position_bc_correction2", + 0x10000051: "interpolation_y", + 0x10000052: "camera_binning", + 0x10000053: "camera_supersampling", + 0x10000054: "camera_frame_width", + 0x10000055: "camera_frame_height", + 0x10000056: "camera_offset_x", + 0x10000057: "camera_offset_y", + 0x10000059: "rt_binning", + 0x1000005a: "rt_frame_width", + 0x1000005b: "rt_frame_height", + 0x1000005c: "rt_region_width", + 0x1000005d: "rt_region_height", + 0x1000005e: "rt_offset_x", + 0x1000005f: "rt_offset_y", + 0x10000060: "rt_zoom", + 0x10000061: "rt_line_period", + 0x10000062: "prescan", + 0x10000063: "scan_direction_z", + # track + 0x40000001: "multiplex_type", # 0 after line; 1 after frame + 0x40000002: "multiplex_order", + 0x40000003: "sampling_mode", # 0 sample; 1 line average; 2 frame average + 0x40000004: "sampling_method", # 1 mean; 2 sum + 0x40000005: "sampling_number", + 0x40000006: "acquire", + 0x40000007: "sample_observation_time", + 0x4000000b: "time_between_stacks", + 0x4000000c: "name", + 0x4000000d: "collimator1_name", + 0x4000000e: "collimator1_position", + 0x4000000f: "collimator2_name", + 0x40000010: "collimator2_position", + 0x40000011: "is_bleach_track", + 0x40000012: "is_bleach_after_scan_number", + 0x40000013: "bleach_scan_number", + 0x40000014: "trigger_in", + 0x40000015: "trigger_out", + 0x40000016: "is_ratio_track", + 0x40000017: "bleach_count", + 0x40000018: "spi_center_wavelength", + 0x40000019: "pixel_time", + 0x40000021: "condensor_frontlens", + 0x40000023: "field_stop_value", + 0x40000024: "id_condensor_aperture", + 0x40000025: "condensor_aperture", + 0x40000026: "id_condensor_revolver", + 0x40000027: "condensor_filter", + 0x40000028: "id_transmission_filter1", + 0x40000029: "id_transmission1", + 0x40000030: "id_transmission_filter2", + 0x40000031: "id_transmission2", + 0x40000032: "repeat_bleach", + 0x40000033: "enable_spot_bleach_pos", + 0x40000034: "spot_bleach_posx", + 0x40000035: "spot_bleach_posy", + 0x40000036: "spot_bleach_posz", + 0x40000037: "id_tubelens", + 0x40000038: "id_tubelens_position", + 0x40000039: "transmitted_light", + 0x4000003a: "reflected_light", + 0x4000003b: "simultan_grab_and_bleach", + 0x4000003c: "bleach_pixel_time", + # laser + 0x50000001: "name", + 0x50000002: "acquire", + 0x50000003: "power", + # detection_channel + 0x70000001: "integration_mode", + 0x70000002: "special_mode", + 0x70000003: "detector_gain_first", + 0x70000004: "detector_gain_last", + 0x70000005: "amplifier_gain_first", + 0x70000006: "amplifier_gain_last", + 0x70000007: "amplifier_offs_first", + 0x70000008: "amplifier_offs_last", + 0x70000009: "pinhole_diameter", + 0x7000000a: "counting_trigger", + 0x7000000b: "acquire", + 0x7000000c: "point_detector_name", + 0x7000000d: "amplifier_name", + 0x7000000e: "pinhole_name", + 0x7000000f: "filter_set_name", + 0x70000010: "filter_name", + 0x70000013: "integrator_name", + 0x70000014: "channel_name", + 0x70000015: "detector_gain_bc1", + 0x70000016: "detector_gain_bc2", + 0x70000017: "amplifier_gain_bc1", + 0x70000018: "amplifier_gain_bc2", + 0x70000019: "amplifier_offset_bc1", + 0x70000020: "amplifier_offset_bc2", + 0x70000021: "spectral_scan_channels", + 0x70000022: "spi_wavelength_start", + 0x70000023: "spi_wavelength_stop", + 0x70000026: "dye_name", + 0x70000027: "dye_folder", + # illumination_channel + 0x90000001: "name", + 0x90000002: "power", + 0x90000003: "wavelength", + 0x90000004: "aquire", + 0x90000005: "detchannel_name", + 0x90000006: "power_bc1", + 0x90000007: "power_bc2", + # beam_splitter + 0xb0000001: "filter_set", + 0xb0000002: "filter", + 0xb0000003: "name", + # data_channel + 0xd0000001: "name", + 0xd0000003: "acquire", + 0xd0000004: "color", + 0xd0000005: "sample_type", + 0xd0000006: "bits_per_sample", + 0xd0000007: "ratio_type", + 0xd0000008: "ratio_track1", + 0xd0000009: "ratio_track2", + 0xd000000a: "ratio_channel1", + 0xd000000b: "ratio_channel2", + 0xd000000c: "ratio_const1", + 0xd000000d: "ratio_const2", + 0xd000000e: "ratio_const3", + 0xd000000f: "ratio_const4", + 0xd0000010: "ratio_const5", + 0xd0000011: "ratio_const6", + 0xd0000012: "ratio_first_images1", + 0xd0000013: "ratio_first_images2", + 0xd0000014: "dye_name", + 0xd0000015: "dye_folder", + 0xd0000016: "spectrum", + 0xd0000017: "acquire", + # timer + 0x12000001: "name", + 0x12000002: "description", + 0x12000003: "interval", + 0x12000004: "trigger_in", + 0x12000005: "trigger_out", + 0x12000006: "activation_time", + 0x12000007: "activation_number", + # marker + 0x14000001: "name", + 0x14000002: "description", + 0x14000003: "trigger_in", + 0x14000004: "trigger_out", +} + +# Map TIFF tag code to attribute name, default value, type, count, validator +TIFF_TAGS = { + 254: ('new_subfile_type', 0, 4, 1, TIFF_SUBFILE_TYPES()), + 255: ('subfile_type', None, 3, 1, + {0: 'undefined', 1: 'image', 2: 'reduced_image', 3: 'page'}), + 256: ('image_width', None, 4, 1, None), + 257: ('image_length', None, 4, 1, None), + 258: ('bits_per_sample', 1, 3, 1, None), + 259: ('compression', 1, 3, 1, TIFF_COMPESSIONS), + 262: ('photometric', None, 3, 1, TIFF_PHOTOMETRICS), + 266: ('fill_order', 1, 3, 1, {1: 'msb2lsb', 2: 'lsb2msb'}), + 269: ('document_name', None, 2, None, None), + 270: ('image_description', None, 2, None, None), + 271: ('make', None, 2, None, None), + 272: ('model', None, 2, None, None), + 273: ('strip_offsets', None, 4, None, None), + 274: ('orientation', 1, 3, 1, TIFF_ORIENTATIONS), + 277: ('samples_per_pixel', 1, 3, 1, None), + 278: ('rows_per_strip', 2**32-1, 4, 1, None), + 279: ('strip_byte_counts', None, 4, None, None), + 280: ('min_sample_value', None, 3, None, None), + 281: ('max_sample_value', None, 3, None, None), # 2**bits_per_sample + 282: ('x_resolution', None, 5, 1, None), + 283: ('y_resolution', None, 5, 1, None), + 284: ('planar_configuration', 1, 3, 1, {1: 'contig', 2: 'separate'}), + 285: ('page_name', None, 2, None, None), + 286: ('x_position', None, 5, 1, None), + 287: ('y_position', None, 5, 1, None), + 296: ('resolution_unit', 2, 4, 1, {1: 'none', 2: 'inch', 3: 'centimeter'}), + 297: ('page_number', None, 3, 2, None), + 305: ('software', None, 2, None, None), + 306: ('datetime', None, 2, None, None), + 315: ('artist', None, 2, None, None), + 316: ('host_computer', None, 2, None, None), + 317: ('predictor', 1, 3, 1, {1: None, 2: 'horizontal'}), + 318: ('white_point', None, 5, 2, None), + 319: ('primary_chromaticities', None, 5, 6, None), + 320: ('color_map', None, 3, None, None), + 322: ('tile_width', None, 4, 1, None), + 323: ('tile_length', None, 4, 1, None), + 324: ('tile_offsets', None, 4, None, None), + 325: ('tile_byte_counts', None, 4, None, None), + 338: ('extra_samples', None, 3, None, + {0: 'unspecified', 1: 'assocalpha', 2: 'unassalpha'}), + 339: ('sample_format', 1, 3, 1, TIFF_SAMPLE_FORMATS), + 340: ('smin_sample_value', None, None, None, None), + 341: ('smax_sample_value', None, None, None, None), + 347: ('jpeg_tables', None, 7, None, None), + 530: ('ycbcr_subsampling', 1, 3, 2, None), + 531: ('ycbcr_positioning', 1, 3, 1, None), + 32996: ('sgi_matteing', None, None, 1, None), # use extra_samples + 32996: ('sgi_datatype', None, None, 1, None), # use sample_format + 32997: ('image_depth', None, 4, 1, None), + 32998: ('tile_depth', None, 4, 1, None), + 33432: ('copyright', None, 1, None, None), + 33445: ('md_file_tag', None, 4, 1, None), + 33446: ('md_scale_pixel', None, 5, 1, None), + 33447: ('md_color_table', None, 3, None, None), + 33448: ('md_lab_name', None, 2, None, None), + 33449: ('md_sample_info', None, 2, None, None), + 33450: ('md_prep_date', None, 2, None, None), + 33451: ('md_prep_time', None, 2, None, None), + 33452: ('md_file_units', None, 2, None, None), + 33550: ('model_pixel_scale', None, 12, 3, None), + 33922: ('model_tie_point', None, 12, None, None), + 34665: ('exif_ifd', None, None, 1, None), + 34735: ('geo_key_directory', None, 3, None, None), + 34736: ('geo_double_params', None, 12, None, None), + 34737: ('geo_ascii_params', None, 2, None, None), + 34853: ('gps_ifd', None, None, 1, None), + 37510: ('user_comment', None, None, None, None), + 42112: ('gdal_metadata', None, 2, None, None), + 42113: ('gdal_nodata', None, 2, None, None), + 50289: ('mc_xy_position', None, 12, 2, None), + 50290: ('mc_z_position', None, 12, 1, None), + 50291: ('mc_xy_calibration', None, 12, 3, None), + 50292: ('mc_lens_lem_na_n', None, 12, 3, None), + 50293: ('mc_channel_name', None, 1, None, None), + 50294: ('mc_ex_wavelength', None, 12, 1, None), + 50295: ('mc_time_stamp', None, 12, 1, None), + 50838: ('imagej_byte_counts', None, None, None, None), + 65200: ('flex_xml', None, 2, None, None), + # code: (attribute name, default value, type, count, validator) +} + +# Map custom TIFF tag codes to attribute names and import functions +CUSTOM_TAGS = { + 700: ('xmp', read_bytes), + 34377: ('photoshop', read_numpy), + 33723: ('iptc', read_bytes), + 34675: ('icc_profile', read_bytes), + 33628: ('uic1tag', read_uic1tag), # Universal Imaging Corporation STK + 33629: ('uic2tag', read_uic2tag), + 33630: ('uic3tag', read_uic3tag), + 33631: ('uic4tag', read_uic4tag), + 34361: ('mm_header', read_mm_header), # Olympus FluoView + 34362: ('mm_stamp', read_mm_stamp), + 34386: ('mm_user_block', read_bytes), + 34412: ('cz_lsm_info', read_cz_lsm_info), # Carl Zeiss LSM + 43314: ('nih_image_header', read_nih_image_header), + # 40001: ('mc_ipwinscal', read_bytes), + 40100: ('mc_id_old', read_bytes), + 50288: ('mc_id', read_bytes), + 50296: ('mc_frame_properties', read_bytes), + 50839: ('imagej_metadata', read_bytes), + 51123: ('micromanager_metadata', read_json), +} + +# Max line length of printed output +PRINT_LINE_LEN = 79 + + +def imshow(data, title=None, vmin=0, vmax=None, cmap=None, + bitspersample=None, photometric='rgb', interpolation='nearest', + dpi=96, figure=None, subplot=111, maxdim=8192, **kwargs): + """Plot n-dimensional images using matplotlib.pyplot. + + Return figure, subplot and plot axis. + Requires pyplot already imported ``from matplotlib import pyplot``. + + Parameters + ---------- + bitspersample : int or None + Number of bits per channel in integer RGB images. + photometric : {'miniswhite', 'minisblack', 'rgb', or 'palette'} + The color space of the image data. + title : str + Window and subplot title. + figure : matplotlib.figure.Figure (optional). + Matplotlib to use for plotting. + subplot : int + A matplotlib.pyplot.subplot axis. + maxdim : int + maximum image size in any dimension. + kwargs : optional + Arguments for matplotlib.pyplot.imshow. + + """ + #if photometric not in ('miniswhite', 'minisblack', 'rgb', 'palette'): + # raise ValueError("Can't handle %s photometrics" % photometric) + # TODO: handle photometric == 'separated' (CMYK) + isrgb = photometric in ('rgb', 'palette') + data = numpy.atleast_2d(data.squeeze()) + data = data[(slice(0, maxdim), ) * len(data.shape)] + + dims = data.ndim + if dims < 2: + raise ValueError("not an image") + elif dims == 2: + dims = 0 + isrgb = False + else: + if isrgb and data.shape[-3] in (3, 4): + data = numpy.swapaxes(data, -3, -2) + data = numpy.swapaxes(data, -2, -1) + elif not isrgb and (data.shape[-1] < data.shape[-2] // 16 and + data.shape[-1] < data.shape[-3] // 16 and + data.shape[-1] < 5): + data = numpy.swapaxes(data, -3, -1) + data = numpy.swapaxes(data, -2, -1) + isrgb = isrgb and data.shape[-1] in (3, 4) + dims -= 3 if isrgb else 2 + + if photometric == 'palette' and isrgb: + datamax = data.max() + if datamax > 255: + data >>= 8 # possible precision loss + data = data.astype('B') + elif data.dtype.kind in 'ui': + if not (isrgb and data.dtype.itemsize <= 1) or bitspersample is None: + try: + bitspersample = int(math.ceil(math.log(data.max(), 2))) + except Exception: + bitspersample = data.dtype.itemsize * 8 + elif not isinstance(bitspersample, int): + # bitspersample can be tuple, e.g. (5, 6, 5) + bitspersample = data.dtype.itemsize * 8 + datamax = 2**bitspersample + if isrgb: + if bitspersample < 8: + data <<= 8 - bitspersample + elif bitspersample > 8: + data >>= bitspersample - 8 # precision loss + data = data.astype('B') + elif data.dtype.kind == 'f': + datamax = data.max() + if isrgb and datamax > 1.0: + if data.dtype.char == 'd': + data = data.astype('f') + data /= datamax + elif data.dtype.kind == 'b': + datamax = 1 + elif data.dtype.kind == 'c': + raise NotImplementedError("complex type") # TODO: handle complex types + + if not isrgb: + if vmax is None: + vmax = datamax + if vmin is None: + if data.dtype.kind == 'i': + dtmin = numpy.iinfo(data.dtype).min + vmin = numpy.min(data) + if vmin == dtmin: + vmin = numpy.min(data > dtmin) + if data.dtype.kind == 'f': + dtmin = numpy.finfo(data.dtype).min + vmin = numpy.min(data) + if vmin == dtmin: + vmin = numpy.min(data > dtmin) + else: + vmin = 0 + + pyplot = sys.modules['matplotlib.pyplot'] + + if figure is None: + pyplot.rc('font', family='sans-serif', weight='normal', size=8) + figure = pyplot.figure(dpi=dpi, figsize=(10.3, 6.3), frameon=True, + facecolor='1.0', edgecolor='w') + try: + figure.canvas.manager.window.title(title) + except Exception: + pass + pyplot.subplots_adjust(bottom=0.03*(dims+2), top=0.9, + left=0.1, right=0.95, hspace=0.05, wspace=0.0) + subplot = pyplot.subplot(subplot) + + if title: + try: + title = unicode(title, 'Windows-1252') + except TypeError: + pass + pyplot.title(title, size=11) + + if cmap is None: + if data.dtype.kind in 'ubf' or vmin == 0: + cmap = 'cubehelix' + else: + cmap = 'coolwarm' + if photometric == 'miniswhite': + cmap += '_r' + + image = pyplot.imshow(data[(0, ) * dims].squeeze(), vmin=vmin, vmax=vmax, + cmap=cmap, interpolation=interpolation, **kwargs) + + if not isrgb: + pyplot.colorbar() # panchor=(0.55, 0.5), fraction=0.05 + + def format_coord(x, y): + # callback function to format coordinate display in toolbar + x = int(x + 0.5) + y = int(y + 0.5) + try: + if dims: + return "%s @ %s [%4i, %4i]" % (cur_ax_dat[1][y, x], + current, x, y) + else: + return "%s @ [%4i, %4i]" % (data[y, x], x, y) + except IndexError: + return "" + + pyplot.gca().format_coord = format_coord + + if dims: + current = list((0, ) * dims) + cur_ax_dat = [0, data[tuple(current)].squeeze()] + sliders = [pyplot.Slider( + pyplot.axes([0.125, 0.03*(axis+1), 0.725, 0.025]), + 'Dimension %i' % axis, 0, data.shape[axis]-1, 0, facecolor='0.5', + valfmt='%%.0f [%i]' % data.shape[axis]) for axis in range(dims)] + for slider in sliders: + slider.drawon = False + + def set_image(current, sliders=sliders, data=data): + # change image and redraw canvas + cur_ax_dat[1] = data[tuple(current)].squeeze() + image.set_data(cur_ax_dat[1]) + for ctrl, index in zip(sliders, current): + ctrl.eventson = False + ctrl.set_val(index) + ctrl.eventson = True + figure.canvas.draw() + + def on_changed(index, axis, data=data, current=current): + # callback function for slider change event + index = int(round(index)) + cur_ax_dat[0] = axis + if index == current[axis]: + return + if index >= data.shape[axis]: + index = 0 + elif index < 0: + index = data.shape[axis] - 1 + current[axis] = index + set_image(current) + + def on_keypressed(event, data=data, current=current): + # callback function for key press event + key = event.key + axis = cur_ax_dat[0] + if str(key) in '0123456789': + on_changed(key, axis) + elif key == 'right': + on_changed(current[axis] + 1, axis) + elif key == 'left': + on_changed(current[axis] - 1, axis) + elif key == 'up': + cur_ax_dat[0] = 0 if axis == len(data.shape)-1 else axis + 1 + elif key == 'down': + cur_ax_dat[0] = len(data.shape)-1 if axis == 0 else axis - 1 + elif key == 'end': + on_changed(data.shape[axis] - 1, axis) + elif key == 'home': + on_changed(0, axis) + + figure.canvas.mpl_connect('key_press_event', on_keypressed) + for axis, ctrl in enumerate(sliders): + ctrl.on_changed(lambda k, a=axis: on_changed(k, a)) + + return figure, subplot, image + + +def _app_show(): + """Block the GUI. For use as skimage plugin.""" + pyplot = sys.modules['matplotlib.pyplot'] + pyplot.show() + + +def main(argv=None): + """Command line usage main function.""" + if float(sys.version[0:3]) < 2.6: + print("This script requires Python version 2.6 or better.") + print("This is Python version %s" % sys.version) + return 0 + if argv is None: + argv = sys.argv + + import optparse + + parser = optparse.OptionParser( + usage="usage: %prog [options] path", + description="Display image data in TIFF files.", + version="%%prog %s" % __version__) + opt = parser.add_option + opt('-p', '--page', dest='page', type='int', default=-1, + help="display single page") + opt('-s', '--series', dest='series', type='int', default=-1, + help="display series of pages of same shape") + opt('--nomultifile', dest='nomultifile', action='store_true', + default=False, help="don't read OME series from multiple files") + opt('--noplot', dest='noplot', action='store_true', default=False, + help="don't display images") + opt('--interpol', dest='interpol', metavar='INTERPOL', default='bilinear', + help="image interpolation method") + opt('--dpi', dest='dpi', type='int', default=96, + help="set plot resolution") + opt('--debug', dest='debug', action='store_true', default=False, + help="raise exception on failures") + opt('--test', dest='test', action='store_true', default=False, + help="try read all images in path") + opt('--doctest', dest='doctest', action='store_true', default=False, + help="runs the docstring examples") + opt('-v', '--verbose', dest='verbose', action='store_true', default=True) + opt('-q', '--quiet', dest='verbose', action='store_false') + + settings, path = parser.parse_args() + path = ' '.join(path) + + if settings.doctest: + import doctest + doctest.testmod() + return 0 + if not path: + parser.error("No file specified") + if settings.test: + test_tifffile(path, settings.verbose) + return 0 + + if any(i in path for i in '?*'): + path = glob.glob(path) + if not path: + print('no files match the pattern') + return 0 + # TODO: handle image sequences + #if len(path) == 1: + path = path[0] + + print("Reading file structure...", end=' ') + start = time.time() + try: + tif = TiffFile(path, multifile=not settings.nomultifile) + except Exception as e: + if settings.debug: + raise + else: + print("\n", e) + sys.exit(0) + print("%.3f ms" % ((time.time()-start) * 1e3)) + + if tif.is_ome: + settings.norgb = True + + images = [(None, tif[0 if settings.page < 0 else settings.page])] + if not settings.noplot: + print("Reading image data... ", end=' ') + + def notnone(x): + return next(i for i in x if i is not None) + start = time.time() + try: + if settings.page >= 0: + images = [(tif.asarray(key=settings.page), + tif[settings.page])] + elif settings.series >= 0: + images = [(tif.asarray(series=settings.series), + notnone(tif.series[settings.series].pages))] + else: + images = [] + for i, s in enumerate(tif.series): + try: + images.append( + (tif.asarray(series=i), notnone(s.pages))) + except ValueError as e: + images.append((None, notnone(s.pages))) + if settings.debug: + raise + else: + print("\n* series %i failed: %s... " % (i, e), + end='') + print("%.3f ms" % ((time.time()-start) * 1e3)) + except Exception as e: + if settings.debug: + raise + else: + print(e) + + tif.close() + + print("\nTIFF file:", tif) + print() + for i, s in enumerate(tif.series): + print ("Series %i" % i) + print(s) + print() + for i, page in images: + print(page) + print(page.tags) + if page.is_palette: + print("\nColor Map:", page.color_map.shape, page.color_map.dtype) + for attr in ('cz_lsm_info', 'cz_lsm_scan_info', 'uic_tags', + 'mm_header', 'imagej_tags', 'micromanager_metadata', + 'nih_image_header'): + if hasattr(page, attr): + print("", attr.upper(), Record(getattr(page, attr)), sep="\n") + print() + if page.is_micromanager: + print('MICROMANAGER_FILE_METADATA') + print(Record(tif.micromanager_metadata)) + + if images and not settings.noplot: + try: + import matplotlib + matplotlib.use('TkAgg') + from matplotlib import pyplot + except ImportError as e: + warnings.warn("failed to import matplotlib.\n%s" % e) + else: + for img, page in images: + if img is None: + continue + vmin, vmax = None, None + if 'gdal_nodata' in page.tags: + try: + vmin = numpy.min(img[img > float(page.gdal_nodata)]) + except ValueError: + pass + if page.is_stk: + try: + vmin = page.uic_tags['min_scale'] + vmax = page.uic_tags['max_scale'] + except KeyError: + pass + else: + if vmax <= vmin: + vmin, vmax = None, None + title = "%s\n %s" % (str(tif), str(page)) + imshow(img, title=title, vmin=vmin, vmax=vmax, + bitspersample=page.bits_per_sample, + photometric=page.photometric, + interpolation=settings.interpol, + dpi=settings.dpi) + pyplot.show() + + +TIFFfile = TiffFile # backwards compatibility + +if sys.version_info[0] > 2: + basestring = str, bytes + unicode = str + +if __name__ == "__main__": + sys.exit(main()) + diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 885ce0c2..fcc26e9e 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -14,7 +14,7 @@ except ImportError: from skimage.util import img_as_ubyte, img_as_uint from six import string_types -from tifffile import imread as tif_imread, imsave as tif_imsave +from skimage.external.tifffile import imread as tif_imread, imsave as tif_imsave def imread(fname, dtype=None): diff --git a/skimage/io/_plugins/tifffile_plugin.py b/skimage/io/_plugins/tifffile_plugin.py index 58038db4..efdddf1b 100644 --- a/skimage/io/_plugins/tifffile_plugin.py +++ b/skimage/io/_plugins/tifffile_plugin.py @@ -1,6 +1 @@ -try: - from tifffile import imread, imsave -except ImportError: - raise ImportError("The tifffile module could not be found.\n" - "It can be obtained at " - "\n") +from skimage.external.tifffile import imread, imsave From dddbc1aa23ac626d9593f1e6ad60cecc59045efb Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 15 Oct 2014 05:57:56 -0500 Subject: [PATCH 0584/1122] Add the __init__.py to make it a package --- skimage/external/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 skimage/external/__init__.py diff --git a/skimage/external/__init__.py b/skimage/external/__init__.py new file mode 100644 index 00000000..e69de29b From fbba5e5e015d1d9d03380745686f01acb409d8c8 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 15 Oct 2014 05:58:54 -0500 Subject: [PATCH 0585/1122] Remove tifffile as an external dependency --- DEPENDS.txt | 1 - requirements.txt | 1 - 2 files changed, 2 deletions(-) diff --git a/DEPENDS.txt b/DEPENDS.txt index e5c10521..aa3cd021 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -16,7 +16,6 @@ Runtime requirements * `NetworkX `__ * `Pillow `__ (or `PIL `__) -* `Tifffile `__ Known build errors ------------------ diff --git a/requirements.txt b/requirements.txt index 7ce05c95..98f3b77f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,3 @@ scipy>=0.9 six>=1.3 networkx>=1.8 pillow>=1.1.7 -tifffile>=0.2 From b6f775a7cf8c775d89e8fe203cd2e0f874be5965 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 15 Oct 2014 06:03:11 -0500 Subject: [PATCH 0586/1122] Use new wheels site and do not install tifffile until optional --- .travis.yml | 2 +- tools/travis_install_optional.sh | 2 ++ tools/travis_setup.sh | 2 -- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7a0e2b94..fb99c279 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ python: before_install: - export DISPLAY=:99.0 - export PYTHONWARNINGS="all" - - export WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" + - export WHEELHOUSE="--no-index --find-links=http://travis-wheels.scikit-image.org/" - travis_retry tools/travis_setup.sh install: diff --git a/tools/travis_install_optional.sh b/tools/travis_install_optional.sh index a373cc6a..9ecb9d20 100755 --- a/tools/travis_install_optional.sh +++ b/tools/travis_install_optional.sh @@ -30,3 +30,5 @@ pip install astropy if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then pip install pyamg fi + +pip install tifffile diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh index cefe3a73..f3caaa5e 100755 --- a/tools/travis_setup.sh +++ b/tools/travis_setup.sh @@ -15,8 +15,6 @@ if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then pip install https://github.com/networkx/networkx/archive/networkx-1.8.tar.gz fi -pip install $WHEELHOUSE numpy -pip install tifffile pip install -r requirements.txt $WHEELHOUSE python check_bento_build.py From 50aff0058a0b4801f0ec83ce442279fb211ea97d Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 15 Oct 2014 20:59:20 -0500 Subject: [PATCH 0587/1122] Add tifffile.c to the build --- bento.info | 3 + skimage/external/setup.py | 26 + skimage/external/tifffile/tifffile.c | 962 ++++++++++++++++++ .../tifffile/{_tifffile.py => tiffile_.py} | 2 + skimage/setup.py | 1 + 5 files changed, 994 insertions(+) create mode 100644 skimage/external/setup.py create mode 100644 skimage/external/tifffile/tifffile.c rename skimage/external/tifffile/{_tifffile.py => tiffile_.py} (99%) diff --git a/bento.info b/bento.info index 564f1f5a..7518abbc 100644 --- a/bento.info +++ b/bento.info @@ -157,6 +157,9 @@ Library: Extension: skimage.graph._ncut_cy Sources: skimage/graph/_ncut_cy.pyx + Extension: skimage.external.tifffile._tifffile + Sources: + skimage/external/tifffile/_tifffile.c Executable: skivi Module: skimage.scripts.skivi diff --git a/skimage/external/setup.py b/skimage/external/setup.py new file mode 100644 index 00000000..c046cd62 --- /dev/null +++ b/skimage/external/setup.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python + +import os.path + +base_path = os.path.abspath(os.path.dirname(__file__)) + + +def configuration(parent_package='', top_path=None): + from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs + + config = Configuration('external', parent_package, top_path) + + config.add_extension('tifffile._tifffile', + sources=['tifffile/_tifffile.c'], + include_dirs=[get_numpy_include_dirs()]) + return config + +if __name__ == '__main__': + from numpy.distutils.core import setup + setup(maintainer='scikit-image Developers', + maintainer_email='scikit-image@googlegroups.com', + description='External Libaries', + url='https://github.com/scikit-image/scikit-image', + license='Modified BSD', + **(configuration(top_path='').todict()) + ) diff --git a/skimage/external/tifffile/tifffile.c b/skimage/external/tifffile/tifffile.c new file mode 100644 index 00000000..6aa0eaa1 --- /dev/null +++ b/skimage/external/tifffile/tifffile.c @@ -0,0 +1,962 @@ + + +/* tifffile.c + +A Python C extension module for decoding PackBits and LZW encoded TIFF data. + +Refer to the tifffile.py module for documentation and tests. + +:Author: + `Christoph Gohlke `_ + +:Organization: + Laboratory for Fluorescence Dynamics, University of California, Irvine + +:Version: 2013.11.05 + +Install +------- +Use this Python distutils setup script to build the extension module:: + + # setup.py + # Usage: ``python setup.py build_ext --inplace`` + from distutils.core import setup, Extension + import numpy + setup(name='_tifffile', + ext_modules=[Extension('_tifffile', ['tifffile.c'], + include_dirs=[numpy.get_include()])]) + +License +------- +Copyright (c) 2008-2014, Christoph Gohlke +Copyright (c) 2008-2014, The Regents of the University of California +Produced at the Laboratory for Fluorescence Dynamics +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of the copyright holders nor the names of any + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#define _VERSION_ "2013.11.05" + +#define WIN32_LEAN_AND_MEAN +#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION + +#include "Python.h" +#include "string.h" +#include "numpy/arrayobject.h" + +/* little endian by default */ +#ifndef MSB +#define MSB 1 +#endif + +#if MSB +#define LSB 0 +#define BOC '<' +#else +#define LSB 1 +#define BOC '>' +#endif + +#define NO_ERROR 0 +#define VALUE_ERROR -1 + +#ifdef _MSC_VER +typedef unsigned __int8 uint8_t; +typedef unsigned __int16 uint16_t; +typedef unsigned __int32 uint32_t; +typedef unsigned __int64 uint64_t; +#ifdef _WIN64 +typedef __int64 ssize_t; +typedef signed __int64 intptr_t; +typedef unsigned __int64 uintptr_t; +#define SSIZE_MAX (9223372036854775808) +#else +typedef int ssize_t; +typedef _W64 signed int intptr_t; +typedef _W64 unsigned int uintptr_t; +#define SSIZE_MAX (2147483648) +#endif +#else +/* non MS compilers */ +#include +#include +#endif + +#define SWAP2BYTES(x) \ + ((((x) >> 8) & 0x00FF) | (((x) & 0x00FF) << 8)) + +#define SWAP4BYTES(x) \ + ((((x) >> 24) & 0x00FF) | (((x)&0x00FF) << 24) | \ + (((x) >> 8 ) & 0xFF00) | (((x)&0xFF00) << 8)) + +#define SWAP8BYTES(x) \ + ((((x) >> 56) & 0x00000000000000FF) | (((x) >> 40) & 0x000000000000FF00) | \ + (((x) >> 24) & 0x0000000000FF0000) | (((x) >> 8) & 0x00000000FF000000) | \ + (((x) << 8) & 0x000000FF00000000) | (((x) << 24) & 0x0000FF0000000000) | \ + (((x) << 40) & 0x00FF000000000000) | (((x) << 56) & 0xFF00000000000000)) + +struct BYTE_STRING { + unsigned int ref; /* reference count */ + unsigned int len; /* length of string */ + char *str; /* pointer to bytes */ +}; + +typedef union { + uint8_t b[2]; + uint16_t i; +} u_uint16; + +typedef union { + uint8_t b[4]; + uint32_t i; +} u_uint32; + +typedef union { + uint8_t b[8]; + uint64_t i; +} u_uint64; + +/*****************************************************************************/ +/* C functions */ + +/* Return mask for itemsize bits */ +unsigned char bitmask(const int itemsize) { + unsigned char result = 0; + unsigned char power = 1; + int i; + for (i = 0; i < itemsize; i++) { + result += power; + power *= 2; + } + return result << (8 - itemsize); +} + +/** Unpack sequence of tigthly packed 1-32 bit integers. + +Native byte order will be returned. + +Input data array should be padded to the next 16, 32 or 64-bit boundary +if itemsize not in (1, 2, 4, 8, 16, 24, 32, 64). + +*/ +int unpackbits( + unsigned char *data, + const ssize_t size, /** size of data in bytes */ + const int itemsize, /** number of bits in integer */ + ssize_t numitems, /** number of items to unpack */ + unsigned char *result /** buffer to store unpacked items */ + ) +{ + ssize_t i, j, k, storagesize; + unsigned char value; + /* Input validation is done in wrapper function */ + storagesize = (ssize_t)(ceil(itemsize / 8.0)); + storagesize = storagesize < 3 ? storagesize : storagesize > 4 ? 8 : 4; + switch (itemsize) { + case 8: + case 16: + case 32: + case 64: + memcpy(result, data, numitems*storagesize); + return NO_ERROR; + case 1: + for (i = 0, j = 0; i < numitems/8; i++) { + value = data[i]; + result[j++] = (value & (unsigned char)(128)) >> 7; + result[j++] = (value & (unsigned char)(64)) >> 6; + result[j++] = (value & (unsigned char)(32)) >> 5; + result[j++] = (value & (unsigned char)(16)) >> 4; + result[j++] = (value & (unsigned char)(8)) >> 3; + result[j++] = (value & (unsigned char)(4)) >> 2; + result[j++] = (value & (unsigned char)(2)) >> 1; + result[j++] = (value & (unsigned char)(1)); + } + if (numitems % 8) { + value = data[i]; + switch (numitems % 8) { + case 7: result[j+6] = (value & (unsigned char)(2)) >> 1; + case 6: result[j+5] = (value & (unsigned char)(4)) >> 2; + case 5: result[j+4] = (value & (unsigned char)(8)) >> 3; + case 4: result[j+3] = (value & (unsigned char)(16)) >> 4; + case 3: result[j+2] = (value & (unsigned char)(32)) >> 5; + case 2: result[j+1] = (value & (unsigned char)(64)) >> 6; + case 1: result[j] = (value & (unsigned char)(128)) >> 7; + } + } + return NO_ERROR; + case 2: + for (i = 0, j = 0; i < numitems/4; i++) { + value = data[i]; + result[j++] = (value & (unsigned char)(192)) >> 6; + result[j++] = (value & (unsigned char)(48)) >> 4; + result[j++] = (value & (unsigned char)(12)) >> 2; + result[j++] = (value & (unsigned char)(3)); + } + if (numitems % 4) { + value = data[i]; + switch (numitems % 4) { + case 3: result[j+2] = (value & (unsigned char)(12)) >> 2; + case 2: result[j+1] = (value & (unsigned char)(48)) >> 4; + case 1: result[j] = (value & (unsigned char)(192)) >> 6; + } + } + return NO_ERROR; + case 4: + for (i = 0, j = 0; i < numitems/2; i++) { + value = data[i]; + result[j++] = (value & (unsigned char)(240)) >> 4; + result[j++] = (value & (unsigned char)(15)); + } + if (numitems % 2) { + value = data[i]; + result[j] = (value & (unsigned char)(240)) >> 4; + } + return NO_ERROR; + case 24: + j = k = 0; + for (i = 0; i < numitems; i++) { + result[j++] = 0; + result[j++] = data[k++]; + result[j++] = data[k++]; + result[j++] = data[k++]; + } + return NO_ERROR; + } + /* 3, 5, 6, 7 */ + if (itemsize < 8) { + int shr = 16; + u_uint16 value, mask, tmp; + j = k = 0; + value.b[MSB] = data[j++]; + value.b[LSB] = data[j++]; + mask.b[MSB] = bitmask(itemsize); + mask.b[LSB] = 0; + for (i = 0; i < numitems; i++) { + shr -= itemsize; + tmp.i = (value.i & mask.i) >> shr; + result[k++] = tmp.b[LSB]; + if (shr < itemsize) { + value.b[MSB] = value.b[LSB]; + value.b[LSB] = data[j++]; + mask.i <<= 8 - itemsize; + shr += 8; + } else { + mask.i >>= itemsize; + } + } + return NO_ERROR; + } + /* 9, 10, 11, 12, 13, 14, 15 */ + if (itemsize < 16) { + int shr = 32; + u_uint32 value, mask, tmp; + mask.i = 0; + j = k = 0; +#if MSB + for (i = 3; i >= 0; i--) { + value.b[i] = data[j++]; + } + mask.b[3] = 0xFF; + mask.b[2] = bitmask(itemsize-8); + for (i = 0; i < numitems; i++) { + shr -= itemsize; + tmp.i = (value.i & mask.i) >> shr; + result[k++] = tmp.b[0]; /* swap bytes */ + result[k++] = tmp.b[1]; + if (shr < itemsize) { + value.b[3] = value.b[1]; + value.b[2] = value.b[0]; + value.b[1] = data[j++]; + value.b[0] = data[j++]; + mask.i <<= 16 - itemsize; + shr += 16; + } else { + mask.i >>= itemsize; + } + } +#else + /* not implemented */ +#endif + return NO_ERROR; + } + /* 17, 18, 19, 20, 21, 22, 23, 25, 26, 27, 28, 29, 30, 31 */ + if (itemsize < 32) { + int shr = 64; + u_uint64 value, mask, tmp; + mask.i = 0; + j = k = 0; +#if MSB + for (i = 7; i >= 0; i--) { + value.b[i] = data[j++]; + } + mask.b[7] = 0xFF; + mask.b[6] = 0xFF; + mask.b[5] = itemsize > 23 ? 0xFF : bitmask(itemsize-16); + mask.b[4] = itemsize < 24 ? 0x00 : bitmask(itemsize-24); + for (i = 0; i < numitems; i++) { + shr -= itemsize; + tmp.i = (value.i & mask.i) >> shr; + result[k++] = tmp.b[0]; /* swap bytes */ + result[k++] = tmp.b[1]; + result[k++] = tmp.b[2]; + result[k++] = tmp.b[3]; + if (shr < itemsize) { + value.b[7] = value.b[3]; + value.b[6] = value.b[2]; + value.b[5] = value.b[1]; + value.b[4] = value.b[0]; + value.b[3] = data[j++]; + value.b[2] = data[j++]; + value.b[1] = data[j++]; + value.b[0] = data[j++]; + mask.i <<= 32 - itemsize; + shr += 32; + } else { + mask.i >>= itemsize; + } + } +#else + /* Not implemented */ +#endif + return NO_ERROR; + } + return VALUE_ERROR; +} + +/*****************************************************************************/ +/* Python functions */ + +/** Unpack tightly packed integers. */ +char py_unpackints_doc[] = "Unpack groups of bits into numpy array."; + +static PyObject* +py_unpackints(PyObject *obj, PyObject *args, PyObject *kwds) +{ + PyObject *byteobj = NULL; + PyArrayObject *result = NULL; + PyArray_Descr *dtype = NULL; + char *encoded = NULL; + char *decoded = NULL; + Py_ssize_t encoded_len = 0; + Py_ssize_t decoded_len = 0; + Py_ssize_t runlen = 0; + Py_ssize_t i; + int storagesize, bytesize; + int itemsize = 0; + int skipbits = 0; + static char *kwlist[] = {"data", "dtype", "itemsize", "runlen", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO&i|i", kwlist, + &byteobj, PyArray_DescrConverter, &dtype, &itemsize, &runlen)) + return NULL; + + Py_INCREF(byteobj); + + if (((itemsize < 1) || (itemsize > 32)) && (itemsize != 64)) { + PyErr_Format(PyExc_ValueError, "itemsize out of range"); + goto _fail; + } + + if (!PyBytes_Check(byteobj)) { + PyErr_Format(PyExc_TypeError, "expected byte string as input"); + goto _fail; + } + + encoded = PyBytes_AS_STRING(byteobj); + encoded_len = PyBytes_GET_SIZE(byteobj); + bytesize = (int)ceil(itemsize / 8.0); + storagesize = bytesize < 3 ? bytesize : bytesize > 4 ? 8 : 4; + if ((encoded_len < bytesize) || (encoded_len > SSIZE_MAX / storagesize)) { + PyErr_Format(PyExc_ValueError, "data size out of range"); + goto _fail; + } + if (dtype->elsize != storagesize) { + PyErr_Format(PyExc_TypeError, "dtype.elsize doesn't fit itemsize"); + goto _fail; + } + + if (runlen == 0) { + runlen = (Py_ssize_t)(((uint64_t)encoded_len*8) / (uint64_t)itemsize); + } + skipbits = (Py_ssize_t)(((uint64_t)runlen * (uint64_t)itemsize) % 8); + if (skipbits > 0) { + skipbits = 8 - skipbits; + } + decoded_len = (Py_ssize_t)((uint64_t)runlen * (((uint64_t)encoded_len*8) / + ((uint64_t)runlen*(uint64_t)itemsize + (uint64_t)skipbits))); + + result = (PyArrayObject *)PyArray_SimpleNew(1, &decoded_len, + dtype->type_num); + if (result == NULL) { + PyErr_Format(PyExc_MemoryError, "unable to allocate output array"); + goto _fail; + } + decoded = (char *)PyArray_DATA(result); + + for (i = 0; i < decoded_len; i+=runlen) { + if (NO_ERROR != + unpackbits((unsigned char *) encoded, + (ssize_t) encoded_len, + (int) itemsize, + (ssize_t) runlen, + (unsigned char *) decoded)) { + PyErr_Format(PyExc_ValueError, "unpackbits() failed"); + goto _fail; + } + encoded += (Py_ssize_t)(((uint64_t)runlen * (uint64_t)itemsize + + (uint64_t)skipbits) / 8); + decoded += runlen * storagesize; + } + + if ((dtype->byteorder != BOC) && (itemsize % 8 == 0)) { + switch (dtype->elsize) { + case 2: { + uint16_t *d = (uint16_t *)PyArray_DATA(result); + for (i = 0; i < PyArray_SIZE(result); i++) { + *d = SWAP2BYTES(*d); d++; + } + break; } + case 4: { + uint32_t *d = (uint32_t *)PyArray_DATA(result); + for (i = 0; i < PyArray_SIZE(result); i++) { + *d = SWAP4BYTES(*d); d++; + } + break; } + case 8: { + uint64_t *d = (uint64_t *)PyArray_DATA(result); + for (i = 0; i < PyArray_SIZE(result); i++) { + *d = SWAP8BYTES(*d); d++; + } + break; } + } + } + Py_DECREF(byteobj); + Py_DECREF(dtype); + return PyArray_Return(result); + + _fail: + Py_XDECREF(byteobj); + Py_XDECREF(result); + Py_XDECREF(dtype); + return NULL; +} + + +/** Decode TIFF PackBits encoded string. */ +char py_decodepackbits_doc[] = "Return TIFF PackBits decoded string."; + +static PyObject * +py_decodepackbits(PyObject *obj, PyObject *args) +{ + int n; + char e; + char *decoded = NULL; + char *encoded = NULL; + char *encoded_end = NULL; + char *encoded_pos = NULL; + unsigned int encoded_len; + unsigned int decoded_len; + PyObject *byteobj = NULL; + PyObject *result = NULL; + + if (!PyArg_ParseTuple(args, "O", &byteobj)) + return NULL; + + if (!PyBytes_Check(byteobj)) { + PyErr_Format(PyExc_TypeError, "expected byte string as input"); + goto _fail; + } + + Py_INCREF(byteobj); + encoded = PyBytes_AS_STRING(byteobj); + encoded_len = (unsigned int)PyBytes_GET_SIZE(byteobj); + + /* release GIL: byte/string objects are immutable */ + Py_BEGIN_ALLOW_THREADS + + /* determine size of decoded string */ + encoded_pos = encoded; + encoded_end = encoded + encoded_len; + decoded_len = 0; + while (encoded_pos < encoded_end) { + n = (int)*encoded_pos++; + if (n >= 0) { + n++; + if (encoded_pos+n > encoded_end) + n = (int)(encoded_end - encoded_pos); + encoded_pos += n; + decoded_len += n; + } else if (n > -128) { + encoded_pos++; + decoded_len += 1-n; + } + } + Py_END_ALLOW_THREADS + + result = PyBytes_FromStringAndSize(0, decoded_len); + if (result == NULL) { + PyErr_Format(PyExc_MemoryError, "failed to allocate decoded string"); + goto _fail; + } + decoded = PyBytes_AS_STRING(result); + + Py_BEGIN_ALLOW_THREADS + + /* decode string */ + encoded_end = encoded + encoded_len; + while (encoded < encoded_end) { + n = (int)*encoded++; + if (n >= 0) { + n++; + if (encoded+n > encoded_end) + n = (int)(encoded_end - encoded); + /* memmove(decoded, encoded, n); decoded += n; encoded += n; */ + while (n--) + *decoded++ = *encoded++; + } else if (n > -128) { + n = 1 - n; + e = *encoded++; + /* memset(decoded, e, n); decoded += n; */ + while (n--) + *decoded++ = e; + } + } + Py_END_ALLOW_THREADS + + Py_DECREF(byteobj); + return result; + + _fail: + Py_XDECREF(byteobj); + Py_XDECREF(result); + return NULL; +} + + +/** Decode TIFF LZW encoded string. */ +char py_decodelzw_doc[] = "Return TIFF LZW decoded string."; + +static PyObject * +py_decodelzw(PyObject *obj, PyObject *args) +{ + PyThreadState *_save = NULL; + PyObject *byteobj = NULL; + PyObject *result = NULL; + int i, j; + unsigned int encoded_len = 0; + unsigned int decoded_len = 0; + unsigned int result_len = 0; + unsigned int table_len = 0; + unsigned int len; + unsigned int code, c, oldcode, mask, shr; + uint64_t bitcount, bitw; + char *encoded = NULL; + char *result_ptr = NULL; + char *table2 = NULL; + char *cptr; + struct BYTE_STRING *decoded = NULL; + struct BYTE_STRING *decoded_ptr = NULL; + struct BYTE_STRING *table[4096]; + struct BYTE_STRING *newentry, *newresult, *t; + int little_endian = 0; + + if (!PyArg_ParseTuple(args, "O", &byteobj)) + return NULL; + + if (!PyBytes_Check(byteobj)) { + PyErr_Format(PyExc_TypeError, "expected byte string as input"); + goto _fail; + } + + Py_INCREF(byteobj); + encoded = PyBytes_AS_STRING(byteobj); + encoded_len = (unsigned int)PyBytes_GET_SIZE(byteobj); + /* + if (encoded_len >= 512 * 1024 * 1024) { + PyErr_Format(PyExc_ValueError, "encoded data > 512 MB not supported"); + goto _fail; + } + */ + /* release GIL: byte/string objects are immutable */ + _save = PyEval_SaveThread(); + + if ((*encoded != -128) || ((*(encoded+1) & 128))) { + PyEval_RestoreThread(_save); + PyErr_Format(PyExc_ValueError, + "strip must begin with CLEAR code"); + goto _fail; + } + little_endian = (*(unsigned short *)encoded) & 128; + + /* allocate buffer for codes and pointers */ + decoded_len = 0; + len = (encoded_len + encoded_len/9) * sizeof(decoded); + decoded = PyMem_Malloc(len * sizeof(void *)); + if (decoded == NULL) { + PyEval_RestoreThread(_save); + PyErr_Format(PyExc_MemoryError, "failed to allocate decoded"); + goto _fail; + } + memset((void *)decoded, 0, len * sizeof(void *)); + decoded_ptr = decoded; + + /* cache strings of length 2 */ + cptr = table2 = PyMem_Malloc(256*256*2 * sizeof(char)); + if (table2 == NULL) { + PyEval_RestoreThread(_save); + PyErr_Format(PyExc_MemoryError, "failed to allocate table2"); + goto _fail; + } + for (i = 0; i < 256; i++) { + for (j = 0; j < 256; j++) { + *cptr++ = (char)i; + *cptr++ = (char)j; + } + } + + memset(table, 0, sizeof(table)); + table_len = 258; + bitw = 9; + shr = 23; + mask = 4286578688; + bitcount = 0; + result_len = 0; + code = 0; + oldcode = 0; + + while ((unsigned int)((bitcount + bitw) / 8) <= encoded_len) { + /* read next code */ + code = *((unsigned int *)((void *)(encoded + (bitcount / 8)))); + if (little_endian) + code = SWAP4BYTES(code); + code <<= (unsigned int)(bitcount % 8); + code &= mask; + code >>= shr; + bitcount += bitw; + + if (code == 257) /* end of information */ + break; + + if (code == 256) { /* clearcode */ + /* initialize table and switch to 9 bit */ + while (table_len > 258) { + t = table[--table_len]; + t->ref--; + if (t->ref == 0) { + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + } + bitw = 9; + shr = 23; + mask = 4286578688; + + /* read next code */ + code = *((unsigned int *)((void *)(encoded + (bitcount / 8)))); + if (little_endian) + code = SWAP4BYTES(code); + code <<= bitcount % 8; + code &= mask; + code >>= shr; + bitcount += bitw; + + if (code == 257) /* end of information */ + break; + + /* decoded.append(table[code]) */ + if (code < 256) { + result_len++; + *((int *)decoded_ptr++) = code; + } else { + newresult = table[code]; + newresult->ref++; + result_len += newresult->len; + *(struct BYTE_STRING **)decoded_ptr++ = newresult; + } + } else { + if (code < table_len) { + /* code is in table */ + /* newresult = table[code]; */ + /* newentry = table[oldcode] + table[code][0] */ + /* decoded.append(newresult); table.append(newentry) */ + if (code < 256) { + c = code; + *((unsigned int *)decoded_ptr++) = code; + result_len++; + } else { + newresult = table[code]; + newresult->ref++; + c = (unsigned int) *newresult->str; + *(struct BYTE_STRING **)decoded_ptr++ = newresult; + result_len += newresult->len; + } + newentry = PyMem_Malloc(sizeof(struct BYTE_STRING)); + newentry->ref = 1; + if (oldcode < 256) { + newentry->len = 2; + newentry->str = table2 + (oldcode << 9) + + ((unsigned char)c << 1); + } else { + len = table[oldcode]->len; + newentry->len = len + 1; + newentry->str = PyMem_Malloc(newentry->len); + if (newentry->str == NULL) + break; + memmove(newentry->str, table[oldcode]->str, len); + newentry->str[len] = c; + } + table[table_len++] = newentry; + } else { + /* code is not in table */ + /* newentry = newresult = table[oldcode] + table[oldcode][0] */ + /* decoded.append(newresult); table.append(newentry) */ + newresult = PyMem_Malloc(sizeof(struct BYTE_STRING)); + newentry = newresult; + newentry->ref = 2; + if (oldcode < 256) { + newentry->len = 2; + newentry->str = table2 + 514*oldcode; + } else { + len = table[oldcode]->len; + newentry->len = len + 1; + newentry->str = PyMem_Malloc(newentry->len); + if (newentry->str == NULL) + break; + memmove(newentry->str, table[oldcode]->str, len); + newentry->str[len] = *table[oldcode]->str; + } + table[table_len++] = newentry; + *(struct BYTE_STRING **)decoded_ptr++ = newresult; + result_len += newresult->len; + } + } + oldcode = code; + /* increase bit-width if necessary */ + switch (table_len) { + case 511: + bitw = 10; + shr = 22; + mask = 4290772992; + break; + case 1023: + bitw = 11; + shr = 21; + mask = 4292870144; + break; + case 2047: + bitw = 12; + shr = 20; + mask = 4293918720; + } + } + + PyEval_RestoreThread(_save); + + if (code != 257) { + PyErr_WarnEx(NULL, + "py_decodelzw encountered unexpected end of stream", 1); + } + + /* result = ''.join(decoded) */ + decoded_len = (unsigned int)(decoded_ptr - decoded); + decoded_ptr = decoded; + result = PyBytes_FromStringAndSize(0, result_len); + if (result == NULL) { + PyErr_Format(PyExc_MemoryError, "failed to allocate decoded string"); + goto _fail; + } + result_ptr = PyBytes_AS_STRING(result); + + _save = PyEval_SaveThread(); + + while (decoded_len--) { + code = *((unsigned int *)decoded_ptr); + if (code < 256) { + *result_ptr++ = (char)code; + } else { + t = *((struct BYTE_STRING **)decoded_ptr); + memmove(result_ptr, t->str, t->len); + result_ptr += t->len; + if (--t->ref == 0) { + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + } + decoded_ptr++; + } + PyMem_Free(decoded); + + while (table_len-- > 258) { + t = table[table_len]; + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + PyMem_Free(table2); + + PyEval_RestoreThread(_save); + + Py_DECREF(byteobj); + return result; + + _fail: + if (table2 != NULL) + PyMem_Free(table2); + if (decoded != NULL) { + /* Bug? are decoded_ptr and decoded_len correct? */ + while (decoded_len--) { + code = *((unsigned int *) decoded_ptr); + if (code > 258) { + t = *((struct BYTE_STRING **) decoded_ptr); + if (--t->ref == 0) { + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + } + } + PyMem_Free(decoded); + } + while (table_len-- > 258) { + t = table[table_len]; + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + + Py_XDECREF(byteobj); + Py_XDECREF(result); + + return NULL; +} + +/*****************************************************************************/ +/* Create Python module */ + +char module_doc[] = + "A Python C extension module for decoding PackBits and LZW encoded " + "TIFF data.\n\n" + "Refer to the tifffile.py module for documentation and tests.\n\n" + "Authors:\n Christoph Gohlke \n" + " Laboratory for Fluorescence Dynamics, University of California, Irvine." + "\n\nVersion: %s\n"; + +static PyMethodDef module_methods[] = { +#if MSB + {"unpackints", (PyCFunction)py_unpackints, METH_VARARGS|METH_KEYWORDS, + py_unpackints_doc}, +#endif + {"decodelzw", (PyCFunction)py_decodelzw, METH_VARARGS, + py_decodelzw_doc}, + {"decodepackbits", (PyCFunction)py_decodepackbits, METH_VARARGS, + py_decodepackbits_doc}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +#if PY_MAJOR_VERSION >= 3 + +struct module_state { + PyObject *error; +}; + +#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) + +static int module_traverse(PyObject *m, visitproc visit, void *arg) { + Py_VISIT(GETSTATE(m)->error); + return 0; +} + +static int module_clear(PyObject *m) { + Py_CLEAR(GETSTATE(m)->error); + return 0; +} + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_tifffile", + NULL, + sizeof(struct module_state), + module_methods, + NULL, + module_traverse, + module_clear, + NULL +}; + +#define INITERROR return NULL + +PyMODINIT_FUNC +PyInit__tifffile(void) + +#else + +#define INITERROR return + +PyMODINIT_FUNC +init_tifffile(void) + +#endif +{ + PyObject *module; + + char *doc = (char *)PyMem_Malloc(sizeof(module_doc) + sizeof(_VERSION_)); + PyOS_snprintf(doc, sizeof(doc), module_doc, _VERSION_); + +#if PY_MAJOR_VERSION >= 3 + moduledef.m_doc = doc; + module = PyModule_Create(&moduledef); +#else + module = Py_InitModule3("_tifffile", module_methods, doc); +#endif + + PyMem_Free(doc); + + if (module == NULL) + INITERROR; + + if (_import_array() < 0) { + Py_DECREF(module); + INITERROR; + } + + { +#if PY_MAJOR_VERSION < 3 + PyObject *s = PyString_FromString(_VERSION_); +#else + PyObject *s = PyUnicode_FromString(_VERSION_); +#endif + PyObject *dict = PyModule_GetDict(module); + PyDict_SetItemString(dict, "__version__", s); + Py_DECREF(s); + } + +#if PY_MAJOR_VERSION >= 3 + return module; +#endif +} + diff --git a/skimage/external/tifffile/_tifffile.py b/skimage/external/tifffile/tiffile_.py similarity index 99% rename from skimage/external/tifffile/_tifffile.py rename to skimage/external/tifffile/tiffile_.py index 83ae4f0e..b2d3303a 100644 --- a/skimage/external/tifffile/_tifffile.py +++ b/skimage/external/tifffile/tiffile_.py @@ -151,6 +151,8 @@ from xml.etree import cElementTree as etree import numpy +from . import _tifffile + __version__ = '0.3.1' __docformat__ = 'restructuredtext en' __all__ = ('imsave', 'imread', 'imshow', 'TiffFile', 'TiffWriter', diff --git a/skimage/setup.py b/skimage/setup.py index 962adb97..9a8c8ac2 100644 --- a/skimage/setup.py +++ b/skimage/setup.py @@ -21,6 +21,7 @@ def configuration(parent_package='', top_path=None): config.add_subpackage('transform') config.add_subpackage('util') config.add_subpackage('segmentation') + config.add_subpackage('external') def add_test_directories(arg, dirname, fnames): if dirname.split(os.path.sep)[-1] == 'tests': From 88719b33402986137d070d9dd3dac2bc86de4932 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 15 Oct 2014 21:00:52 -0500 Subject: [PATCH 0588/1122] Fix path to tifffile.c --- skimage/external/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/external/setup.py b/skimage/external/setup.py index c046cd62..343c4d54 100644 --- a/skimage/external/setup.py +++ b/skimage/external/setup.py @@ -11,7 +11,7 @@ def configuration(parent_package='', top_path=None): config = Configuration('external', parent_package, top_path) config.add_extension('tifffile._tifffile', - sources=['tifffile/_tifffile.c'], + sources=['tifffile/tifffile.c'], include_dirs=[get_numpy_include_dirs()]) return config From a3c8ce32ac9d3a142f3aa42d532510e130811998 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 15 Oct 2014 21:05:23 -0500 Subject: [PATCH 0589/1122] Rename file to a valid module name. --- skimage/external/tifffile/__init__.py | 2 +- skimage/external/tifffile/{tiffile_.py => tifffile_local.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename skimage/external/tifffile/{tiffile_.py => tifffile_local.py} (100%) diff --git a/skimage/external/tifffile/__init__.py b/skimage/external/tifffile/__init__.py index da90abdf..9ca9ec84 100644 --- a/skimage/external/tifffile/__init__.py +++ b/skimage/external/tifffile/__init__.py @@ -1,4 +1,4 @@ try: from tifffile import * except ImportError: - from ._tifffile import * + from .tifffile_local import * diff --git a/skimage/external/tifffile/tiffile_.py b/skimage/external/tifffile/tifffile_local.py similarity index 100% rename from skimage/external/tifffile/tiffile_.py rename to skimage/external/tifffile/tifffile_local.py From 284cdd0bcac4c10098d68466b4d28655bc525226 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 16 Oct 2014 03:20:09 -0500 Subject: [PATCH 0590/1122] Do not test or cover the external libraries directly --- .coveragerc | 2 +- tools/travis_test.sh | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.coveragerc b/.coveragerc index 992eafdc..ffab8979 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,7 +6,7 @@ source = skimage include = */skimage/* omit = */setup.py - */skimage/io/_plugins/tifffile.py + */skimage/external/* [report] exclude_lines = diff --git a/tools/travis_test.sh b/tools/travis_test.sh index 4c2db692..78d406a7 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -40,10 +40,9 @@ echo 'backend.qt4 : '$MPL_QT_API >> $MPL_DIR/matplotlibrc tools/header.py "Run tests with all dependencies" # run tests again with optional dependencies to get more coverage +TEST_ARGS='--exe -v --with-doctest --ignore-files="skimage/external/*"' if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then - export TEST_ARGS="--with-cov --cover-package skimage" -else - export TEST_ARGS="" + TEST_ARGS="$TEST_ARGS --with-cov --cover-package skimage" fi -nosetests --exe -v --with-doctest $TEST_ARGS +nosetests $TEST_ARGS From 0569524d7929da0d1ada6cc815ec14b46bd806a0 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 16 Oct 2014 04:14:42 -0500 Subject: [PATCH 0591/1122] Use proper nose option --- tools/travis_test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/travis_test.sh b/tools/travis_test.sh index 78d406a7..2b8588de 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -40,7 +40,7 @@ echo 'backend.qt4 : '$MPL_QT_API >> $MPL_DIR/matplotlibrc tools/header.py "Run tests with all dependencies" # run tests again with optional dependencies to get more coverage -TEST_ARGS='--exe -v --with-doctest --ignore-files="skimage/external/*"' +TEST_ARGS='--exe -v --with-doctest --exclude="skimage/external/*"' if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then TEST_ARGS="$TEST_ARGS --with-cov --cover-package skimage" fi From b5da90e4c7126037b47e452460d04939af967a10 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 16 Oct 2014 04:49:45 -0500 Subject: [PATCH 0592/1122] Just disable doctests in tifffile_local --- skimage/external/tifffile/tifffile_local.py | 72 ++++++++++----------- tools/travis_test.sh | 2 +- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/skimage/external/tifffile/tifffile_local.py b/skimage/external/tifffile/tifffile_local.py index b2d3303a..17d430b0 100644 --- a/skimage/external/tifffile/tifffile_local.py +++ b/skimage/external/tifffile/tifffile_local.py @@ -116,13 +116,13 @@ References Examples -------- ->>> data = numpy.random.rand(5, 301, 219) ->>> imsave('temp.tif', data) +>> data = numpy.random.rand(5, 301, 219) +>> imsave('temp.tif', data) ->>> image = imread('temp.tif') ->>> numpy.testing.assert_array_equal(image, data) +>> image = imread('temp.tif') +>> numpy.testing.assert_array_equal(image, data) ->>> with TiffFile('temp.tif') as tif: +>> with TiffFile('temp.tif') as tif: ... images = tif.asarray() ... for page in tif: ... for tag in page.tags.values(): @@ -180,9 +180,9 @@ def imsave(filename, data, **kwargs): Examples -------- - >>> data = numpy.random.rand(2, 5, 3, 301, 219) - >>> description = u'{"shape": %s}' % str(list(data.shape)) - >>> imsave('temp.tif', data, compress=6, + >> data = numpy.random.rand(2, 5, 3, 301, 219) + >> description = u'{"shape": %s}' % str(list(data.shape)) + >> imsave('temp.tif', data, compress=6, ... extratags=[(270, 's', 0, description, True)]) """ @@ -209,8 +209,8 @@ class TiffWriter(object): Examples -------- - >>> data = numpy.random.rand(2, 5, 3, 301, 219) - >>> with TiffWriter('temp.tif', bigtiff=True) as tif: + >> data = numpy.random.rand(2, 5, 3, 301, 219) + >> with TiffWriter('temp.tif', bigtiff=True) as tif: ... for i in range(data.shape[0]): ... tif.save(data[i], compress=6) @@ -663,11 +663,11 @@ def imread(files, **kwargs): Examples -------- - >>> im = imread('test.tif', key=0) - >>> im.shape + >> im = imread('test.tif', key=0) + >> im.shape (256, 256, 4) - >>> ims = imread(['test.tif', 'test.tif']) - >>> ims.shape + >> ims = imread(['test.tif', 'test.tif']) + >> ims.shape (2, 256, 256, 4) """ @@ -733,7 +733,7 @@ class TiffFile(object): Examples -------- - >>> with TiffFile('test.tif') as tif: + >> with TiffFile('test.tif') as tif: ... data = tif.asarray() ... data.shape (256, 256, 4) @@ -2215,11 +2215,11 @@ class TiffSequence(object): Examples -------- - >>> tifs = TiffSequence("test.oif.files/*.tif") - >>> tifs.shape, tifs.axes + >> tifs = TiffSequence("test.oif.files/*.tif") + >> tifs.shape, tifs.axes ((2, 100), 'CT') - >>> data = tifs.asarray() - >>> data.shape + >> data = tifs.asarray() + >> data.shape (2, 100, 256, 256) """ @@ -3316,12 +3316,12 @@ def unpackrgb(data, dtype='>> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff) - >>> print(unpackrgb(data, '> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff) + >> print(unpackrgb(data, '>> print(unpackrgb(data, '> print(unpackrgb(data, '>> print(unpackrgb(data, '> print(unpackrgb(data, '>> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC') + >> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC') ((5, 2, 1), 'TYX') """ @@ -3397,7 +3397,7 @@ def transpose_axes(data, axes, asaxes='CTZYX'): A view is returned if possible. - >>> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape + >> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape (5, 2, 1, 3, 4) """ @@ -3448,7 +3448,7 @@ def stripnull(string): Clean NULL terminated C strings. - >>> stripnull(b'string\\x00') + >> stripnull(b'string\\x00') b'string' """ @@ -3461,9 +3461,9 @@ def stripascii(string): Clean NULL separated and terminated TIFF strings. - >>> stripascii(b'string\\x00string\\n\\x01\\x00') + >> stripascii(b'string\\x00string\\n\\x01\\x00') b'string\\x00string\\n' - >>> stripascii(b'\\x00') + >> stripascii(b'\\x00') b'' """ @@ -3490,9 +3490,9 @@ def format_size(size): def sequence(value): """Return tuple containing value if value is not a sequence. - >>> sequence(1) + >> sequence(1) (1,) - >>> sequence([1]) + >> sequence([1]) [1] """ @@ -3508,9 +3508,9 @@ def product(iterable): Equivalent of functools.reduce(operator.mul, iterable, 1). - >>> product([2**8, 2**30]) + >> product([2**8, 2**30]) 274877906944 - >>> product([]) + >> product([]) 1 """ @@ -3525,7 +3525,7 @@ def natural_sorted(iterable): E.g. for sorting file names. - >>> natural_sorted(['f1', 'f2', 'f10']) + >> natural_sorted(['f1', 'f2', 'f10']) ['f1', 'f2', 'f10'] """ @@ -3540,7 +3540,7 @@ def excel_datetime(timestamp, epoch=datetime.datetime.fromordinal(693594)): Convert LSM time stamps. - >>> excel_datetime(40237.029999999795) + >> excel_datetime(40237.029999999795) datetime.datetime(2010, 2, 28, 0, 43, 11, 999982) """ @@ -3552,7 +3552,7 @@ def julian_datetime(julianday, milisecond=0): Convert Julian dates according to MetaMorph. - >>> julian_datetime(2451576, 54362783) + >> julian_datetime(2451576, 54362783) datetime.datetime(2000, 2, 2, 15, 6, 2, 783) """ @@ -3586,7 +3586,7 @@ def test_tifffile(directory='testimages', verbose=True): Print error message on failure. - >>> test_tifffile(verbose=False) + >> test_tifffile(verbose=False) """ successful = 0 diff --git a/tools/travis_test.sh b/tools/travis_test.sh index 2b8588de..dddd6d07 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -40,7 +40,7 @@ echo 'backend.qt4 : '$MPL_QT_API >> $MPL_DIR/matplotlibrc tools/header.py "Run tests with all dependencies" # run tests again with optional dependencies to get more coverage -TEST_ARGS='--exe -v --with-doctest --exclude="skimage/external/*"' +TEST_ARGS='--exe -v --with-doctest' if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then TEST_ARGS="$TEST_ARGS --with-cov --cover-package skimage" fi From 17a3b50a5ed1cc77d438f003ea32d1f4e77d8ca9 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 16 Oct 2014 05:05:37 -0500 Subject: [PATCH 0593/1122] Use tifffile 0.3.2, which passes doctests --- skimage/external/tifffile/tifffile_local.py | 80 ++++++++++----------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/skimage/external/tifffile/tifffile_local.py b/skimage/external/tifffile/tifffile_local.py index 17d430b0..29b4bbdb 100644 --- a/skimage/external/tifffile/tifffile_local.py +++ b/skimage/external/tifffile/tifffile_local.py @@ -116,13 +116,13 @@ References Examples -------- ->> data = numpy.random.rand(5, 301, 219) ->> imsave('temp.tif', data) +>>> data = numpy.random.rand(5, 301, 219) +>>> imsave('temp.tif', data) ->> image = imread('temp.tif') ->> numpy.testing.assert_array_equal(image, data) +>>> image = imread('temp.tif') +>>> numpy.testing.assert_array_equal(image, data) ->> with TiffFile('temp.tif') as tif: +>>> with TiffFile('temp.tif') as tif: ... images = tif.asarray() ... for page in tif: ... for tag in page.tags.values(): @@ -153,7 +153,7 @@ import numpy from . import _tifffile -__version__ = '0.3.1' +__version__ = '0.3.2' __docformat__ = 'restructuredtext en' __all__ = ('imsave', 'imread', 'imshow', 'TiffFile', 'TiffWriter', 'TiffSequence') @@ -180,9 +180,9 @@ def imsave(filename, data, **kwargs): Examples -------- - >> data = numpy.random.rand(2, 5, 3, 301, 219) - >> description = u'{"shape": %s}' % str(list(data.shape)) - >> imsave('temp.tif', data, compress=6, + >>> data = numpy.random.rand(2, 5, 3, 301, 219) + >>> description = u'{"shape": %s}' % str(list(data.shape)) + >>> imsave('temp.tif', data, compress=6, ... extratags=[(270, 's', 0, description, True)]) """ @@ -209,8 +209,8 @@ class TiffWriter(object): Examples -------- - >> data = numpy.random.rand(2, 5, 3, 301, 219) - >> with TiffWriter('temp.tif', bigtiff=True) as tif: + >>> data = numpy.random.rand(2, 5, 3, 301, 219) + >>> with TiffWriter('temp.tif', bigtiff=True) as tif: ... for i in range(data.shape[0]): ... tif.save(data[i], compress=6) @@ -663,12 +663,12 @@ def imread(files, **kwargs): Examples -------- - >> im = imread('test.tif', key=0) - >> im.shape - (256, 256, 4) - >> ims = imread(['test.tif', 'test.tif']) - >> ims.shape - (2, 256, 256, 4) + >>> im = imread('temp.tif', key=0) + >>> im.shape + (3, 301, 219) + >>> ims = imread(['temp.tif', 'temp.tif']) + >>> ims.shape + (2, 10, 3, 301, 219) """ kwargs_file = {} @@ -733,10 +733,10 @@ class TiffFile(object): Examples -------- - >> with TiffFile('test.tif') as tif: + >>> with TiffFile('temp.tif') as tif: ... data = tif.asarray() ... data.shape - (256, 256, 4) + (5, 301, 219) """ def __init__(self, arg, name=None, offset=None, size=None, @@ -2215,11 +2215,11 @@ class TiffSequence(object): Examples -------- - >> tifs = TiffSequence("test.oif.files/*.tif") - >> tifs.shape, tifs.axes + >>> tifs = TiffSequence("test.oif.files/*.tif") # doctest: +SKIP + >>> tifs.shape, tifs.axes # doctest: +SKIP ((2, 100), 'CT') - >> data = tifs.asarray() - >> data.shape + >>> data = tifs.asarray() # doctest: +SKIP + >>> data.shape # doctest: +SKIP (2, 100, 256, 256) """ @@ -3316,12 +3316,12 @@ def unpackrgb(data, dtype='> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff) - >> print(unpackrgb(data, '>> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff) + >>> print(unpackrgb(data, '> print(unpackrgb(data, '>> print(unpackrgb(data, '> print(unpackrgb(data, '>> print(unpackrgb(data, '> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC') + >>> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC') ((5, 2, 1), 'TYX') """ @@ -3397,7 +3397,7 @@ def transpose_axes(data, axes, asaxes='CTZYX'): A view is returned if possible. - >> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape + >>> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape (5, 2, 1, 3, 4) """ @@ -3448,7 +3448,7 @@ def stripnull(string): Clean NULL terminated C strings. - >> stripnull(b'string\\x00') + >>> stripnull(b'string\\x00') b'string' """ @@ -3461,9 +3461,9 @@ def stripascii(string): Clean NULL separated and terminated TIFF strings. - >> stripascii(b'string\\x00string\\n\\x01\\x00') + >>> stripascii(b'string\\x00string\\n\\x01\\x00') b'string\\x00string\\n' - >> stripascii(b'\\x00') + >>> stripascii(b'\\x00') b'' """ @@ -3490,9 +3490,9 @@ def format_size(size): def sequence(value): """Return tuple containing value if value is not a sequence. - >> sequence(1) + >>> sequence(1) (1,) - >> sequence([1]) + >>> sequence([1]) [1] """ @@ -3508,9 +3508,9 @@ def product(iterable): Equivalent of functools.reduce(operator.mul, iterable, 1). - >> product([2**8, 2**30]) + >>> product([2**8, 2**30]) 274877906944 - >> product([]) + >>> product([]) 1 """ @@ -3525,7 +3525,7 @@ def natural_sorted(iterable): E.g. for sorting file names. - >> natural_sorted(['f1', 'f2', 'f10']) + >>> natural_sorted(['f1', 'f2', 'f10']) ['f1', 'f2', 'f10'] """ @@ -3540,7 +3540,7 @@ def excel_datetime(timestamp, epoch=datetime.datetime.fromordinal(693594)): Convert LSM time stamps. - >> excel_datetime(40237.029999999795) + >>> excel_datetime(40237.029999999795) datetime.datetime(2010, 2, 28, 0, 43, 11, 999982) """ @@ -3552,7 +3552,7 @@ def julian_datetime(julianday, milisecond=0): Convert Julian dates according to MetaMorph. - >> julian_datetime(2451576, 54362783) + >>> julian_datetime(2451576, 54362783) datetime.datetime(2000, 2, 2, 15, 6, 2, 783) """ @@ -3586,7 +3586,7 @@ def test_tifffile(directory='testimages', verbose=True): Print error message on failure. - >> test_tifffile(verbose=False) + >>> test_tifffile(verbose=False) """ successful = 0 From 713452ab9eacbcff32dbd1a075aa872883344f40 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 16 Oct 2014 05:28:43 -0500 Subject: [PATCH 0594/1122] Use newer version of tifffile.py. --- skimage/external/tifffile/tifffile_local.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/skimage/external/tifffile/tifffile_local.py b/skimage/external/tifffile/tifffile_local.py index 29b4bbdb..b5a67ccf 100644 --- a/skimage/external/tifffile/tifffile_local.py +++ b/skimage/external/tifffile/tifffile_local.py @@ -153,7 +153,7 @@ import numpy from . import _tifffile -__version__ = '0.3.2' +__version__ = '0.3.3' __docformat__ = 'restructuredtext en' __all__ = ('imsave', 'imread', 'imshow', 'TiffFile', 'TiffWriter', 'TiffSequence') @@ -181,7 +181,7 @@ def imsave(filename, data, **kwargs): Examples -------- >>> data = numpy.random.rand(2, 5, 3, 301, 219) - >>> description = u'{"shape": %s}' % str(list(data.shape)) + >>> description = '{"shape": %s}' % str(list(data.shape)) >>> imsave('temp.tif', data, compress=6, ... extratags=[(270, 's', 0, description, True)]) @@ -3448,7 +3448,7 @@ def stripnull(string): Clean NULL terminated C strings. - >>> stripnull(b'string\\x00') + >>> stripnull(b'string\\x00') # doctest: +SKIP b'string' """ @@ -3461,9 +3461,9 @@ def stripascii(string): Clean NULL separated and terminated TIFF strings. - >>> stripascii(b'string\\x00string\\n\\x01\\x00') + >>> stripascii(b'string\\x00string\\n\\x01\\x00') # doctest: +SKIP b'string\\x00string\\n' - >>> stripascii(b'\\x00') + >>> stripascii(b'\\x00') # doctest: +SKIP b'' """ From 0419c1e6f7d0af99d38ff0a1a02fab4dd9f51655 Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 16 Oct 2014 23:22:56 +0530 Subject: [PATCH 0595/1122] sequential numbering of nodes using max id --- skimage/graph/rag.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index a300c486..1fd1ddc9 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -15,7 +15,6 @@ from scipy.ndimage import filters from scipy import ndimage as nd import math from .. import draw, measure, segmentation, util, color -import random try: from matplotlib import colors from matplotlib import cm @@ -60,6 +59,13 @@ class RAG(nx.Graph): `networx.Graph `_ """ + def __init__(self, data=None, **attr): + + nx.Graph.__init__(self, data, **attr) + self.max_id = None + for n in self.nodes_iter(): + self.max_id = max(self.max_id, n) + def merge_nodes(self, src, dst, weight_func=min_weight, in_place=True, extra_arguments=[], extra_keywords={}): """Merge node `src` and `dst`. @@ -100,10 +106,7 @@ class RAG(nx.Graph): if in_place: new = dst else: - n = self.number_of_nodes() - new = random.randint(1, 2*n) - while new in self: - new = random.randint(1, 2*n) + new = self.max_id + 1 self.add_node(new) for neighbor in neighbors: @@ -118,6 +121,19 @@ class RAG(nx.Graph): self.remove_node(dst) return new + def add_node(self, n, attr_dict=None, **attr): + nx.Graph.add_node(self, n, attr_dict, **attr) + self.max_id = max(n, self.max_id) + + def add_edge(self, u, v, attr_dict=None, **attr): + nx.Graph.add_edge(self, u, v, attr_dict, **attr) + self.max_id = max(u, v, self.max_id) + + def copy(self): + g = nx.Graph.copy(self) + g.max_id = self.max_id + return g + def _add_edge_filter(values, graph): """Create edge in `g` between the first element of `values` and the rest. From 31ecc3db86d216d0fb8687be0a41daeb5d45dd3a Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Fri, 17 Oct 2014 00:06:12 +0530 Subject: [PATCH 0596/1122] Change None to 0 --- skimage/graph/rag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 1fd1ddc9..3200980b 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -62,7 +62,7 @@ class RAG(nx.Graph): def __init__(self, data=None, **attr): nx.Graph.__init__(self, data, **attr) - self.max_id = None + self.max_id = 0 for n in self.nodes_iter(): self.max_id = max(self.max_id, n) From 7734cdf7e02b26952b30445190c1f35dd703ba59 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 16 Oct 2014 17:06:12 -0500 Subject: [PATCH 0597/1122] Move tifffile license description --- skimage/{io => external}/LICENSE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename skimage/{io => external}/LICENSE.txt (91%) diff --git a/skimage/io/LICENSE.txt b/skimage/external/LICENSE.txt similarity index 91% rename from skimage/io/LICENSE.txt rename to skimage/external/LICENSE.txt index 16672bcb..d9f04072 100644 --- a/skimage/io/LICENSE.txt +++ b/skimage/external/LICENSE.txt @@ -1,5 +1,5 @@ -_plugins.tifffile.py +tifffile Copyright (c) 2008-2014, Christoph Gohlke Copyright (c) 2008-2014, The Regents of the University of California From 4d50466edc5b529fb87c4ede8127e3f0922003a1 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 17 Oct 2014 20:13:16 +1100 Subject: [PATCH 0598/1122] Add Travis and Coveralls badges to README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 64a7dfe9..72041a36 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ Image Processing SciKit ======================= +[![Build Status](https://travis-ci.org/scikit-image/scikit-image.svg)](https://travis-ci.org/scikit-image/scikit-image) +[![Coverage Status](https://img.shields.io/coveralls/scikit-image/scikit-image.svg)](https://coveralls.io/r/scikit-image/scikit-image?branch=master) + Source ------ https://github.com/scikit-image/scikit-image From 96bf74719777abd60b15817bf1433d98de707eec Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Sat, 18 Oct 2014 21:33:42 +0530 Subject: [PATCH 0599/1122] always return new id and put new id logic in a function --- skimage/graph/rag.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index 3200980b..fbfe13f9 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -61,10 +61,12 @@ class RAG(nx.Graph): def __init__(self, data=None, **attr): - nx.Graph.__init__(self, data, **attr) - self.max_id = 0 - for n in self.nodes_iter(): - self.max_id = max(self.max_id, n) + super(RAG, self).__init__(data, **attr) + try: + self.max_id = max(self.nodes_iter()) + except ValueError: + # Empty sequence + self.max_id = 0 def merge_nodes(self, src, dst, weight_func=min_weight, in_place=True, extra_arguments=[], extra_keywords={}): @@ -106,7 +108,7 @@ class RAG(nx.Graph): if in_place: new = dst else: - new = self.max_id + 1 + new = self.next_id() self.add_node(new) for neighbor in neighbors: @@ -117,23 +119,37 @@ class RAG(nx.Graph): self.node[new]['labels'] = (self.node[src]['labels'] + self.node[dst]['labels']) self.remove_node(src) + if not in_place: self.remove_node(dst) - return new + + return new def add_node(self, n, attr_dict=None, **attr): - nx.Graph.add_node(self, n, attr_dict, **attr) + super(RAG, self).add_node(n, attr_dict, **attr) self.max_id = max(n, self.max_id) def add_edge(self, u, v, attr_dict=None, **attr): - nx.Graph.add_edge(self, u, v, attr_dict, **attr) + super(RAG, self).add_edge(u, v, attr_dict, **attr) self.max_id = max(u, v, self.max_id) def copy(self): - g = nx.Graph.copy(self) + g = super(RAG, self).copy() g.max_id = self.max_id return g + def next_id(self): + """Returns the `id` for the new node to be inserted. + + The current implementation returns one more than the maximum `id`. + + Returns + ------- + id : int + The `id` of the new node to be inserted. + """ + return self.max_id + 1 + def _add_edge_filter(values, graph): """Create edge in `g` between the first element of `values` and the rest. From 95f4aa70c1bf5e3e471fe7e91bbe091e8afbd41b Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 20 Oct 2014 05:03:00 -0500 Subject: [PATCH 0600/1122] Avoid star import --- skimage/external/tifffile/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/external/tifffile/__init__.py b/skimage/external/tifffile/__init__.py index 9ca9ec84..bd426a3b 100644 --- a/skimage/external/tifffile/__init__.py +++ b/skimage/external/tifffile/__init__.py @@ -1,4 +1,4 @@ try: - from tifffile import * + from tifffile import imread, imsave, TiffFile except ImportError: - from .tifffile_local import * + from .tifffile_local import imread, imsave, TiffFile From 1ae060dce8f1e43145d4946bf36b34b1698a1eaf Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 20 Oct 2014 06:13:00 -0500 Subject: [PATCH 0601/1122] Free disk space during travis install --- tools/travis_setup.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh index f3caaa5e..96231139 100755 --- a/tools/travis_setup.sh +++ b/tools/travis_setup.sh @@ -20,3 +20,7 @@ python check_bento_build.py tools/header.py "Dependency versions" tools/build_versions.py + +# clean up disk space +sudo apt-get clean +sudo rm -r /tmp/* From 79473ee6d92972ba3b7e128b1e7600e7992aea71 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 20 Oct 2014 06:13:39 -0500 Subject: [PATCH 0602/1122] Quiet down optional installs to avoid hitting 10000 line limit --- tools/travis_install_optional.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/travis_install_optional.sh b/tools/travis_install_optional.sh index 9ecb9d20..ec01d899 100755 --- a/tools/travis_install_optional.sh +++ b/tools/travis_install_optional.sh @@ -9,26 +9,26 @@ if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then else sudo apt-get install -q libqt4-dev - pip install PySide $WHEELHOUSE + pip install -q PySide $WHEELHOUSE python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install fi # imread does NOT support py3.2 if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools - pip install imread + pip install -q imread fi # TODO: update when SimpleITK become available on py34 or hopefully pip if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then - easy_install SimpleITK + easy_install -q SimpleITK fi -sudo apt-get install libfreeimage3 -pip install astropy +sudo apt-get install -q libfreeimage3 +pip install -q astropy if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then - pip install pyamg + pip install -q pyamg fi -pip install tifffile +pip install -q tifffile From cb1a26ce2ec2ce8b115682b0adcadc39dd7c83d5 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 20 Oct 2014 22:21:28 -0500 Subject: [PATCH 0603/1122] Fix deprecation of canny function --- skimage/filter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/filter/__init__.py b/skimage/filter/__init__.py index 0228957e..e5b58447 100644 --- a/skimage/filter/__init__.py +++ b/skimage/filter/__init__.py @@ -19,7 +19,7 @@ denoise_tv_chambolle = deprecated('skimage.restoration.denoise_tv_chambolle')\ (restoration.denoise_tv_chambolle) # Backward compatibility v<0.11 -@deprecated +@deprecated('skimage.feature.canny') def canny(*args, **kwargs): # Hack to avoid circular import from skimage.feature._canny import canny as canny_ From b8988190b008b8010594cf3c5c2d08e11a9eea8f Mon Sep 17 00:00:00 2001 From: Vighnesh Birodkar Date: Thu, 23 Oct 2014 13:17:29 +0530 Subject: [PATCH 0604/1122] Docstring changes --- skimage/graph/rag.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/skimage/graph/rag.py b/skimage/graph/rag.py index fbfe13f9..81b3c0d6 100644 --- a/skimage/graph/rag.py +++ b/skimage/graph/rag.py @@ -99,7 +99,12 @@ class RAG(nx.Graph): Returns ------- id : int - The id of the new node if `in_place` is `True`. + The id of the new node. + + Notes + ----- + If `in_place` is `False` the resulting node has a new id, rather than + `dst`. """ src_nbrs = set(self.neighbors(src)) dst_nbrs = set(self.neighbors(dst)) @@ -126,14 +131,23 @@ class RAG(nx.Graph): return new def add_node(self, n, attr_dict=None, **attr): + """Add node `n` while updating the maximum node id. + + .. seealso:: :func:`networkx.Graph.add_node`.""" super(RAG, self).add_node(n, attr_dict, **attr) self.max_id = max(n, self.max_id) def add_edge(self, u, v, attr_dict=None, **attr): + """Add an edge between `u` and `v` while updating max node id. + + .. seealso:: :func:`networkx.Graph.add_edge`.""" super(RAG, self).add_edge(u, v, attr_dict, **attr) self.max_id = max(u, v, self.max_id) def copy(self): + """Copy the graph with its max node id. + + .. seealso:: :func:`networkx.Graph.copy`.""" g = super(RAG, self).copy() g.max_id = self.max_id return g From f736047ebb4ef61409dec231e19b96f103f0f36d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20T=C3=BD=C4=8D?= Date: Sat, 25 Oct 2014 18:34:32 +0200 Subject: [PATCH 0605/1122] Refactored measure.label, added documentation. --- skimage/measure/_ccomp.pyx | 358 +++++++++++++++++++++++++++++-------- 1 file changed, 288 insertions(+), 70 deletions(-) diff --git a/skimage/measure/_ccomp.pyx b/skimage/measure/_ccomp.pyx index 26b9645a..4ccb932a 100644 --- a/skimage/measure/_ccomp.pyx +++ b/skimage/measure/_ccomp.pyx @@ -24,15 +24,166 @@ See also: """ +DTYPE = np.intp + +# Short int - could be more graceful to the CPU cache +ctypedef cnp.int32_t INTS_t + +cdef struct s_shpinfo + +ctypedef s_shpinfo shpinfo +ctypedef s_bginfo bginfo +ctypedef int (* fun_ravel)(int, int, int, shpinfo *) + + +cdef struct s_bginfo: + DTYPE_t background_val + DTYPE_t background_node + + +# Structure for centralised access to shape data +cdef struct s_shpinfo: + INTS_t x + INTS_t y + INTS_t z + + DTYPE_t numels + INTS_t ndim + + #INTS_t Dee + INTS_t Ded + + INTS_t Dea + INTS_t Deb + INTS_t Dec + + INTS_t Def + INTS_t Deg + INTS_t Deh + INTS_t Dei + INTS_t Dej + INTS_t Dek + INTS_t Del + INTS_t Dem + INTS_t Den + + fun_ravel ravel_index + + +cdef shpinfo get_triple(inarr_shape): + cdef shpinfo res = shpinfo() + + res.y = 1 + res.z = 1 + res.ravel_index = ravel_index2D + + res.ndim = len(inarr_shape) + if res.ndim == 1: + res.x = inarr_shape[0] + res.ravel_index = ravel_index1D + elif res.ndim == 2: + res.x = inarr_shape[1] + res.y = inarr_shape[0] + res.ravel_index = ravel_index2D + elif res.ndim == 3: + res.x = inarr_shape[2] + res.y = inarr_shape[1] + res.z = inarr_shape[0] + res.ravel_index = ravel_index3D + else: + assert "Only for images of dimension 1-3 (got %s)" % res.ndim + + res.numels = res.x * res.y * res.z + + # Our point of interest is E. + # z=1 z=0 x + # ----------------------> + # | A B C F G H + # | D E . I J K + # | . . . L M N + # | + # y V + # + # Difference between E and G is (x=0, y=-1, z=-1), E and A (-1, -1, 0) etc. + # Here, it is recalculated to linear (raveled) indices of flattened arrays + # with their last (=contiguous) dimension is x. + + # So now the 1st (needed for 1D, 2D and 3D) part, y = 1, z = 1 + res.Ded = - 1 + + # Not needed, just for illustration + # + enabling it prolongs the exec time quite considerably - why? + #res.Dee = 0 + + # So now the 2nd (needed for 2D and 3D) part, y = 0, z = 1 + res.Dea = res.ravel_index(-1, -1, 0, & res) + res.Deb = res.Dea + 1 + res.Dec = res.Deb + 1 + + # And now the 3rd (needed only for 3D) part, z = 0 + res.Def = res.ravel_index(-1, -1, -1, & res) + res.Deg = res.Def + 1 + res.Deh = res.Def + 2 + res.Dei = res.Def - res.Deb # Deb = one row up, remember? + res.Dej = res.Dei + 1 + res.Dek = res.Dei + 2 + res.Del = res.Dei - 2 * res.Deb + res.Dem = res.Del + 1 + res.Den = res.Del + 2 + + return res + + +cdef int ravel_index1D(int x, int y, int z, shpinfo * shapeinfo): + """ + Ravel index of a 1D array - trivial. y and z are ignored. + """ + return x + + +cdef int ravel_index2D(int x, int y, int z, shpinfo * shapeinfo): + """ + Ravel index of a 2D array. z is ignored + """ + cdef int ret = x + y * shapeinfo.x + return ret + + +cdef int ravel_index3D(int x, int y, int z, shpinfo * shapeinfo): + """ + Ravel index of a 3D array + """ + cdef int ret = x + y * shapeinfo.x + z * shapeinfo.y * shapeinfo.x + return ret + + # Tree operations implemented by an array as described in Wu et al. # The term "forest" is used to indicate an array that stores one or more trees -DTYPE = np.intp - +# Consider a following tree: +# +# 5 ----> 3 ----> 2 ----> 1 <---- 6 <---- 7 +# | | +# 4 >----/ \----< 8 <---- 9 +# +# The vertices are a unique number, so the tree can be represented by an +# array where a the tuple (index, array[index]) represents an edge, +# so for our example, array[2] == 1, array[7] == 6 and array[1] == 1, because +# 1 is the root. +# Last but not least, one array can hold more than one tree as long as their +# indices are different. It is the case in this algorithm, so for that reason +# the array is referred to as the "forrest" = multiple trees next to each +# other. +# +# In this algorithm, there are as many indices as there are elements in the +# array to label and array[x] == x for all x. As the labelling progresses, +# equivalence between so-called provisional (i.e. not final) labels is +# discovered and trees begin to surface. +# When we found out that label 5 and 3 are the same, we assign array[5] = 3. cdef DTYPE_t find_root(DTYPE_t *forest, DTYPE_t n): """Find the root of node n. - + Given the example above, for any integer from 1 to 9, 1 is always returned """ cdef DTYPE_t root = n while (forest[root] < root): @@ -43,7 +194,10 @@ cdef DTYPE_t find_root(DTYPE_t *forest, DTYPE_t n): cdef inline void set_root(DTYPE_t *forest, DTYPE_t n, DTYPE_t root): """ Set all nodes on a path to point to new_root. - + Given the example above, given n=9, root=6, it would "reconnect" the tree. + so forest[9] = 6 and forest[8] = 6 + The ultimate goal is that all tree nodes point to the real root, + which is element 1 in this case. """ cdef DTYPE_t j while (forest[n] < n): @@ -56,12 +210,17 @@ cdef inline void set_root(DTYPE_t *forest, DTYPE_t n, DTYPE_t root): cdef inline void join_trees(DTYPE_t *forest, DTYPE_t n, DTYPE_t m): """Join two trees containing nodes n and m. - + If we imagine that in the example tree, the root 1 is not known, we + rather have two disjoint trees with roots 2 and 6. + Joining them would mean that all elements of both trees become connected + to the element 2, so forest[9] == 2, forest[6] == 2 etc. + However, when the relationship between 1 and 2 can still be discovered later. """ - cdef DTYPE_t root = find_root(forest, n) + cdef DTYPE_t root cdef DTYPE_t root_m if (n != m): + root = find_root(forest, n) root_m = find_root(forest, m) if (root > root_m): @@ -147,94 +306,153 @@ def label(input, DTYPE_t neighbors=8, background=None, return_num=False): [-1 -1 -1]] """ - cdef DTYPE_t rows = input.shape[0] - cdef DTYPE_t cols = input.shape[1] - cdef cnp.ndarray[DTYPE_t, ndim=2] data = np.array(input, copy=True, - dtype=DTYPE) - cdef cnp.ndarray[DTYPE_t, ndim=2] forest + cdef cnp.ndarray[DTYPE_t, ndim=1] data + cdef cnp.ndarray[DTYPE_t, ndim=1] forest - forest = np.arange(data.size, dtype=DTYPE).reshape((rows, cols)) + # Having data a 2D array slows down access considerably using linear + # indices even when using the data_p pointer :-( + data = input.flatten().astype(DTYPE, copy=True) + forest = np.arange(data.size, dtype=DTYPE) cdef DTYPE_t *forest_p = forest.data cdef DTYPE_t *data_p = data.data - cdef DTYPE_t i, j + cdef shpinfo shapeinfo + cdef bginfo bg - cdef DTYPE_t background_val + shapeinfo = get_triple(input.shape) + + bg.background_val = 0 + bg.background_node = -999 if background is None: - background_val = -1 + bg.background_val = -1 warnings.warn(DeprecationWarning( 'The default value for `background` will change to 0 in v0.12' )) else: - background_val = background - - cdef DTYPE_t background_node = -999 + bg.background_val = background if neighbors != 4 and neighbors != 8: raise ValueError('Neighbors must be either 4 or 8.') - # Initialize the first row - if data[0, 0] == background_val: - link_bg(forest_p, 0, &background_node) - - for j in range(1, cols): - if data[0, j] == background_val: - link_bg(forest_p, j, &background_node) - - if data[0, j] == data[0, j-1]: - join_trees(forest_p, j, j-1) - - for i in range(1, rows): - # Handle the first column - if data[i, 0] == background_val: - link_bg(forest_p, i * cols, &background_node) - - if data[i, 0] == data[i-1, 0]: - join_trees(forest_p, i*cols, (i-1)*cols) - - if neighbors == 8: - if data[i, 0] == data[i-1, 1]: - join_trees(forest_p, i*cols, (i-1)*cols + 1) - - for j in range(1, cols): - if data[i, j] == background_val: - link_bg(forest_p, i * cols + j, &background_node) - - if neighbors == 8: - if data[i, j] == data[i-1, j-1]: - join_trees(forest_p, i*cols + j, (i-1)*cols + j - 1) - - if data[i, j] == data[i-1, j]: - join_trees(forest_p, i*cols + j, (i-1)*cols + j) - - if neighbors == 8: - if j < cols - 1: - if data[i, j] == data[i - 1, j + 1]: - join_trees(forest_p, i*cols + j, (i-1)*cols + j + 1) - - if data[i, j] == data[i, j-1]: - join_trees(forest_p, i*cols + j, i*cols + j - 1) + if shapeinfo.ndim == 1: + scan1D(data_p, forest_p, & shapeinfo, & bg, neighbors, 0, 0) + elif shapeinfo.ndim == 2: + scan2D(data_p, forest_p, & shapeinfo, & bg, neighbors, 0) # Label output cdef DTYPE_t ctr = 0 - for i in range(rows): - for j in range(cols): - if (i*cols + j) == background_node: - data[i, j] = -1 - elif (i*cols + j) == forest[i, j]: - data[i, j] = ctr - ctr = ctr + 1 - else: - data[i, j] = data_p[forest[i, j]] + ctr = resolve_labels(data_p, forest_p, & shapeinfo, & bg) # Work around a bug in ndimage's type checking on 32-bit platforms if data.dtype == np.int32: data = data.view(np.int32) + res = data.reshape(input.shape) + if return_num: - return data, ctr + return res, ctr else: - return data + return res + + +cdef DTYPE_t resolve_labels(DTYPE_t * data_p, DTYPE_t * forest_p, + shpinfo * shapeinfo, bginfo * bg): + """ + We iterate through the provisional labels and assign final labels based on + our knowledge of prov. labels relationship. + We also track how many distinct final labels we have. + """ + cdef DTYPE_t counter = 0 + for i in range(shapeinfo.numels): + if i == bg.background_node: + data_p[i] = -1 + elif i == forest_p[i]: + data_p[i] = counter + counter += 1 + else: + data_p[i] = data_p[forest_p[i]] + return counter + + +# Here, we work with flat arrays regardless whether the data is 1, 2 or 3D. +# The lookup to the neighbor in a 2D array is achieved by precalculating an +# offset and ading it to the index. +# The forward scan mask looks like this (the center point is actually E): +# (take a look at shpinfo docs for more exhaustive info) +# A B C +# D E +# +# So if I am in the point E and want to take a look to A, I take the index of +# E and add shapeinfo.Dea to it and teg the index of A. +# The 1D indices are "raveled" or "linear", that's where "rindex" comes from. + + +cdef scan1D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, + bginfo * bg, DTYPE_t neighbors, DTYPE_t y, DTYPE_t z): + """ + Perform forward scan on a 1D object, usually the first row of an image + """ + # Initialize the first row + cdef DTYPE_t x, rindex + rindex = shapeinfo.ravel_index(0, y, z, shapeinfo) + + if data_p[rindex] == bg.background_val: + link_bg(forest_p, rindex, & bg.background_node) + + for x in range(1, shapeinfo.x): + rindex += 1 + # Handle the first row + # First row => rindex == j + if data_p[rindex] == bg.background_val: + link_bg(forest_p, rindex, & bg.background_node) + + if data_p[rindex] == data_p[rindex + shapeinfo.Ded]: + join_trees(forest_p, rindex, rindex + shapeinfo.Ded) + + +cdef scan2D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, + bginfo * bg, DTYPE_t neighbors, DTYPE_t z): + """ + Perform forward scan on a 2D array. + """ + cdef DTYPE_t x, y, rindex + scan1D(data_p, forest_p, shapeinfo, bg, neighbors, 0, z) + for y in range(1, shapeinfo.y): + rindex = shapeinfo.ravel_index(0, y, 0, shapeinfo) + # Handle the first column + if data_p[rindex] == bg.background_val: + link_bg(forest_p, rindex, & bg.background_node) + + if data_p[rindex] == data_p[rindex + shapeinfo.Deb]: + join_trees(forest_p, rindex, rindex + shapeinfo.Deb) + + if neighbors == 8: + if data_p[rindex] == data_p[rindex + shapeinfo.Dec]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dec) + + # Handle the rest of columns + for x in range(1, shapeinfo.x): + # We have just moved to another column (of the same row) + # so we increment the raveled index. It will be reset when we get + # to another row, so we don't have to worry about altering it here. + rindex += 1 + if data_p[rindex] == bg.background_val: + link_bg(forest_p, rindex, & bg.background_node) + + if neighbors == 8: + if data_p[rindex] == data_p[rindex + shapeinfo.Dea]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dea) + + if data_p[rindex] == data_p[rindex + shapeinfo.Deb]: + join_trees(forest_p, rindex, rindex + shapeinfo.Deb) + + if neighbors == 8: + if x < shapeinfo.x - 1: + if data_p[rindex] == data_p[rindex + shapeinfo.Dec]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dec) + + if data_p[rindex] == data_p[rindex + shapeinfo.Ded]: + join_trees(forest_p, rindex, rindex + shapeinfo.Ded) From b65ebdfb13a305c8902dfe29f009537cdd887b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20T=C3=BD=C4=8D?= Date: Sun, 26 Oct 2014 21:30:05 +0100 Subject: [PATCH 0606/1122] Added support for 3D arrays --- skimage/measure/_ccomp.pyx | 150 +++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/skimage/measure/_ccomp.pyx b/skimage/measure/_ccomp.pyx index 4ccb932a..6ba36bf8 100644 --- a/skimage/measure/_ccomp.pyx +++ b/skimage/measure/_ccomp.pyx @@ -341,6 +341,8 @@ def label(input, DTYPE_t neighbors=8, background=None, return_num=False): scan1D(data_p, forest_p, & shapeinfo, & bg, neighbors, 0, 0) elif shapeinfo.ndim == 2: scan2D(data_p, forest_p, & shapeinfo, & bg, neighbors, 0) + elif shapeinfo.ndim == 3: + scan3D(data_p, forest_p, & shapeinfo, & bg, neighbors) # Label output cdef DTYPE_t ctr = 0 @@ -456,3 +458,151 @@ cdef scan2D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, if data_p[rindex] == data_p[rindex + shapeinfo.Ded]: join_trees(forest_p, rindex, rindex + shapeinfo.Ded) + + +cdef scan3D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, + bginfo * bg, DTYPE_t neighbors): + """ + Perform forward scan on a 2D array. + """ + cdef DTYPE_t x, y, z, rindex + # Handle first plane + scan2D(data_p, forest_p, shapeinfo, bg, neighbors, 0) + for z in range(1, shapeinfo.z): + # Handle first row in 3D manner + rindex = shapeinfo.ravel_index(0, 0, z, shapeinfo) + if data_p[rindex] == bg.background_val: + link_bg(forest_p, rindex, & bg.background_node) + + # Now we have pixels below + if data_p[rindex] == data_p[rindex + shapeinfo.Dej]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dej) + + if neighbors == 8: + if data_p[rindex] == data_p[rindex + shapeinfo.Dek]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dek) + + if data_p[rindex] == data_p[rindex + shapeinfo.Dem]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dem) + + if data_p[rindex] == data_p[rindex + shapeinfo.Den]: + join_trees(forest_p, rindex, rindex + shapeinfo.Den) + + for x in range(1, shapeinfo.x): + rindex += 1 + # Handle the first row + if data_p[rindex] == bg.background_val: + link_bg(forest_p, rindex, & bg.background_node) + + if data_p[rindex] == data_p[rindex + shapeinfo.Ded]: + join_trees(forest_p, rindex, rindex + shapeinfo.Ded) + + if neighbors == 8: + if data_p[rindex] == data_p[rindex + shapeinfo.Dei]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dei) + + if data_p[rindex] == data_p[rindex + shapeinfo.Dej]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dej) + + if neighbors == 8: + if x + 1 < shapeinfo.x: + if data_p[rindex] == data_p[rindex + shapeinfo.Dek]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dek) + + if data_p[rindex] == data_p[rindex + shapeinfo.Del]: + join_trees(forest_p, rindex, rindex + shapeinfo.Del) + + if data_p[rindex] == data_p[rindex + shapeinfo.Dem]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dem) + + if x + 1 < shapeinfo.x: + if data_p[rindex] == data_p[rindex + shapeinfo.Den]: + join_trees(forest_p, rindex, rindex + shapeinfo.Den) + + + for y in range(1, shapeinfo.y): + rindex = shapeinfo.ravel_index(0, y, z, shapeinfo) + # Handle the first column in 3D manner + if data_p[rindex] == bg.background_val: + link_bg(forest_p, rindex, & bg.background_node) + + if data_p[rindex] == data_p[rindex + shapeinfo.Deb]: + join_trees(forest_p, rindex, rindex + shapeinfo.Deb) + + if neighbors == 8: + if data_p[rindex] == data_p[rindex + shapeinfo.Dec]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dec) + + if data_p[rindex] == data_p[rindex + shapeinfo.Deg]: + join_trees(forest_p, rindex, rindex + shapeinfo.Deg) + + if data_p[rindex] == data_p[rindex + shapeinfo.Deh]: + join_trees(forest_p, rindex, rindex + shapeinfo.Deh) + + if data_p[rindex] == data_p[rindex + shapeinfo.Dej]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dej) + + if neighbors == 8: + if data_p[rindex] == data_p[rindex + shapeinfo.Dek]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dek) + + if y + 1 < shapeinfo.y: + if data_p[rindex] == data_p[rindex + shapeinfo.Dem]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dem) + + if data_p[rindex] == data_p[rindex + shapeinfo.Den]: + join_trees(forest_p, rindex, rindex + shapeinfo.Den) + + # Handle the rest of columns + for x in range(1, shapeinfo.x): + rindex += 1 + if data_p[rindex] == bg.background_val: + link_bg(forest_p, rindex, & bg.background_node) + + if neighbors == 8: + if data_p[rindex] == data_p[rindex + shapeinfo.Dea]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dea) + + if data_p[rindex] == data_p[rindex + shapeinfo.Deb]: + join_trees(forest_p, rindex, rindex + shapeinfo.Deb) + + if neighbors == 8: + if x < shapeinfo.x - 1: + if data_p[rindex] == data_p[rindex + shapeinfo.Dec]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dec) + + if data_p[rindex] == data_p[rindex + shapeinfo.Ded]: + join_trees(forest_p, rindex, rindex + shapeinfo.Ded) + + # Now pixels below: + if neighbors == 8: + if data_p[rindex] == data_p[rindex + shapeinfo.Def]: + join_trees(forest_p, rindex, rindex + shapeinfo.Def) + + if data_p[rindex] == data_p[rindex + shapeinfo.Deg]: + join_trees(forest_p, rindex, rindex + shapeinfo.Deg) + + if x + 1 < shapeinfo.x: + if data_p[rindex] == data_p[rindex + shapeinfo.Deh]: + join_trees(forest_p, rindex, rindex + shapeinfo.Deh) + + if data_p[rindex] == data_p[rindex + shapeinfo.Dei]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dei) + + if data_p[rindex] == data_p[rindex + shapeinfo.Dej]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dej) + + if neighbors == 8: + if x + 1 < shapeinfo.x: + if data_p[rindex] == data_p[rindex + shapeinfo.Dek]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dek) + + if data_p[rindex] == data_p[rindex + shapeinfo.Del]: + join_trees(forest_p, rindex, rindex + shapeinfo.Del) + + if data_p[rindex] == data_p[rindex + shapeinfo.Dem]: + join_trees(forest_p, rindex, rindex + shapeinfo.Dem) + + if x + 1 < shapeinfo.x: + if data_p[rindex] == data_p[rindex + shapeinfo.Den]: + join_trees(forest_p, rindex, rindex + shapeinfo.Den) From d3049a087cf4b729af6d3460f75bc97a144d0eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20T=C3=BD=C4=8D?= Date: Sun, 26 Oct 2014 23:22:54 +0100 Subject: [PATCH 0607/1122] Refactored Cython stuff with easier code readability in mind --- skimage/measure/_ccomp.pyx | 189 ++++++++++++++----------------------- 1 file changed, 72 insertions(+), 117 deletions(-) diff --git a/skimage/measure/_ccomp.pyx b/skimage/measure/_ccomp.pyx index 6ba36bf8..e537b598 100644 --- a/skimage/measure/_ccomp.pyx +++ b/skimage/measure/_ccomp.pyx @@ -32,15 +32,22 @@ ctypedef cnp.int32_t INTS_t cdef struct s_shpinfo ctypedef s_shpinfo shpinfo -ctypedef s_bginfo bginfo ctypedef int (* fun_ravel)(int, int, int, shpinfo *) -cdef struct s_bginfo: +ctypedef struct bginfo: DTYPE_t background_val DTYPE_t background_node +cdef enum: + # D_ee, # We don't need D_ee + D_ed, + D_ea, D_eb, D_ec, + D_ef, D_eg, D_eh, D_ei, D_ej, D_ek, D_el, D_em, D_en, + D_COUNT + + # Structure for centralised access to shape data cdef struct s_shpinfo: INTS_t x @@ -50,22 +57,7 @@ cdef struct s_shpinfo: DTYPE_t numels INTS_t ndim - #INTS_t Dee - INTS_t Ded - - INTS_t Dea - INTS_t Deb - INTS_t Dec - - INTS_t Def - INTS_t Deg - INTS_t Deh - INTS_t Dei - INTS_t Dej - INTS_t Dek - INTS_t Del - INTS_t Dem - INTS_t Den + INTS_t DEX[D_COUNT] fun_ravel ravel_index @@ -109,31 +101,36 @@ cdef shpinfo get_triple(inarr_shape): # with their last (=contiguous) dimension is x. # So now the 1st (needed for 1D, 2D and 3D) part, y = 1, z = 1 - res.Ded = - 1 + res.DEX[D_ed] = -1 # Not needed, just for illustration - # + enabling it prolongs the exec time quite considerably - why? - #res.Dee = 0 + # res.DEX[D_ee] = 0 # So now the 2nd (needed for 2D and 3D) part, y = 0, z = 1 - res.Dea = res.ravel_index(-1, -1, 0, & res) - res.Deb = res.Dea + 1 - res.Dec = res.Deb + 1 + res.DEX[D_ea] = res.ravel_index(-1, -1, 0, & res) + res.DEX[D_eb] = res.DEX[D_ea] + 1 + res.DEX[D_ec] = res.DEX[D_eb] + 1 # And now the 3rd (needed only for 3D) part, z = 0 - res.Def = res.ravel_index(-1, -1, -1, & res) - res.Deg = res.Def + 1 - res.Deh = res.Def + 2 - res.Dei = res.Def - res.Deb # Deb = one row up, remember? - res.Dej = res.Dei + 1 - res.Dek = res.Dei + 2 - res.Del = res.Dei - 2 * res.Deb - res.Dem = res.Del + 1 - res.Den = res.Del + 2 + res.DEX[D_ef] = res.ravel_index(-1, -1, -1, & res) + res.DEX[D_eg] = res.DEX[D_ef] + 1 + res.DEX[D_eh] = res.DEX[D_ef] + 2 + res.DEX[D_ei] = res.DEX[D_ef] - res.DEX[D_eb] # DEX[D_eb] = one row up, remember? + res.DEX[D_ej] = res.DEX[D_ei] + 1 + res.DEX[D_ek] = res.DEX[D_ei] + 2 + res.DEX[D_el] = res.DEX[D_ei] - 2 * res.DEX[D_eb] + res.DEX[D_em] = res.DEX[D_el] + 1 + res.DEX[D_en] = res.DEX[D_el] + 2 return res +cdef inline void join_trees_wrapper(DTYPE_t * data_p, DTYPE_t * forest_p, + DTYPE_t rindex, INTS_t idxdiff): + if data_p[rindex] == data_p[rindex + idxdiff]: + join_trees(forest_p, rindex, rindex + idxdiff) + + cdef int ravel_index1D(int x, int y, int z, shpinfo * shapeinfo): """ Ravel index of a 1D array - trivial. y and z are ignored. @@ -399,6 +396,7 @@ cdef scan1D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, """ # Initialize the first row cdef DTYPE_t x, rindex + cdef INTS_t * DEX = shapeinfo.DEX rindex = shapeinfo.ravel_index(0, y, z, shapeinfo) if data_p[rindex] == bg.background_val: @@ -411,8 +409,7 @@ cdef scan1D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, if data_p[rindex] == bg.background_val: link_bg(forest_p, rindex, & bg.background_node) - if data_p[rindex] == data_p[rindex + shapeinfo.Ded]: - join_trees(forest_p, rindex, rindex + shapeinfo.Ded) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) cdef scan2D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, @@ -421,6 +418,7 @@ cdef scan2D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, Perform forward scan on a 2D array. """ cdef DTYPE_t x, y, rindex + cdef INTS_t * DEX = shapeinfo.DEX scan1D(data_p, forest_p, shapeinfo, bg, neighbors, 0, z) for y in range(1, shapeinfo.y): rindex = shapeinfo.ravel_index(0, y, 0, shapeinfo) @@ -428,12 +426,10 @@ cdef scan2D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, if data_p[rindex] == bg.background_val: link_bg(forest_p, rindex, & bg.background_node) - if data_p[rindex] == data_p[rindex + shapeinfo.Deb]: - join_trees(forest_p, rindex, rindex + shapeinfo.Deb) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eb]) if neighbors == 8: - if data_p[rindex] == data_p[rindex + shapeinfo.Dec]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dec) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ec]) # Handle the rest of columns for x in range(1, shapeinfo.x): @@ -445,19 +441,15 @@ cdef scan2D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, link_bg(forest_p, rindex, & bg.background_node) if neighbors == 8: - if data_p[rindex] == data_p[rindex + shapeinfo.Dea]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dea) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ea]) - if data_p[rindex] == data_p[rindex + shapeinfo.Deb]: - join_trees(forest_p, rindex, rindex + shapeinfo.Deb) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eb]) if neighbors == 8: if x < shapeinfo.x - 1: - if data_p[rindex] == data_p[rindex + shapeinfo.Dec]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dec) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ec]) - if data_p[rindex] == data_p[rindex + shapeinfo.Ded]: - join_trees(forest_p, rindex, rindex + shapeinfo.Ded) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) cdef scan3D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, @@ -466,6 +458,7 @@ cdef scan3D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, Perform forward scan on a 2D array. """ cdef DTYPE_t x, y, z, rindex + cdef INTS_t * DEX = shapeinfo.DEX # Handle first plane scan2D(data_p, forest_p, shapeinfo, bg, neighbors, 0) for z in range(1, shapeinfo.z): @@ -475,18 +468,12 @@ cdef scan3D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, link_bg(forest_p, rindex, & bg.background_node) # Now we have pixels below - if data_p[rindex] == data_p[rindex + shapeinfo.Dej]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dej) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ej]) if neighbors == 8: - if data_p[rindex] == data_p[rindex + shapeinfo.Dek]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dek) - - if data_p[rindex] == data_p[rindex + shapeinfo.Dem]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dem) - - if data_p[rindex] == data_p[rindex + shapeinfo.Den]: - join_trees(forest_p, rindex, rindex + shapeinfo.Den) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ek]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_em]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_en]) for x in range(1, shapeinfo.x): rindex += 1 @@ -494,30 +481,22 @@ cdef scan3D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, if data_p[rindex] == bg.background_val: link_bg(forest_p, rindex, & bg.background_node) - if data_p[rindex] == data_p[rindex + shapeinfo.Ded]: - join_trees(forest_p, rindex, rindex + shapeinfo.Ded) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) if neighbors == 8: - if data_p[rindex] == data_p[rindex + shapeinfo.Dei]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dei) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ei]) - if data_p[rindex] == data_p[rindex + shapeinfo.Dej]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dej) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ej]) if neighbors == 8: if x + 1 < shapeinfo.x: - if data_p[rindex] == data_p[rindex + shapeinfo.Dek]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dek) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ek]) - if data_p[rindex] == data_p[rindex + shapeinfo.Del]: - join_trees(forest_p, rindex, rindex + shapeinfo.Del) - - if data_p[rindex] == data_p[rindex + shapeinfo.Dem]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dem) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_el]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_em]) if x + 1 < shapeinfo.x: - if data_p[rindex] == data_p[rindex + shapeinfo.Den]: - join_trees(forest_p, rindex, rindex + shapeinfo.Den) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_en]) for y in range(1, shapeinfo.y): @@ -526,32 +505,21 @@ cdef scan3D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, if data_p[rindex] == bg.background_val: link_bg(forest_p, rindex, & bg.background_node) - if data_p[rindex] == data_p[rindex + shapeinfo.Deb]: - join_trees(forest_p, rindex, rindex + shapeinfo.Deb) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eb]) if neighbors == 8: - if data_p[rindex] == data_p[rindex + shapeinfo.Dec]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dec) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ec]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eg]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eh]) - if data_p[rindex] == data_p[rindex + shapeinfo.Deg]: - join_trees(forest_p, rindex, rindex + shapeinfo.Deg) - - if data_p[rindex] == data_p[rindex + shapeinfo.Deh]: - join_trees(forest_p, rindex, rindex + shapeinfo.Deh) - - if data_p[rindex] == data_p[rindex + shapeinfo.Dej]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dej) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ej]) if neighbors == 8: - if data_p[rindex] == data_p[rindex + shapeinfo.Dek]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dek) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ek]) if y + 1 < shapeinfo.y: - if data_p[rindex] == data_p[rindex + shapeinfo.Dem]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dem) - - if data_p[rindex] == data_p[rindex + shapeinfo.Den]: - join_trees(forest_p, rindex, rindex + shapeinfo.Den) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_em]) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_en]) # Handle the rest of columns for x in range(1, shapeinfo.x): @@ -560,49 +528,36 @@ cdef scan3D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, link_bg(forest_p, rindex, & bg.background_node) if neighbors == 8: - if data_p[rindex] == data_p[rindex + shapeinfo.Dea]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dea) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ea]) - if data_p[rindex] == data_p[rindex + shapeinfo.Deb]: - join_trees(forest_p, rindex, rindex + shapeinfo.Deb) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eb]) if neighbors == 8: if x < shapeinfo.x - 1: - if data_p[rindex] == data_p[rindex + shapeinfo.Dec]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dec) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ec]) - if data_p[rindex] == data_p[rindex + shapeinfo.Ded]: - join_trees(forest_p, rindex, rindex + shapeinfo.Ded) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) # Now pixels below: if neighbors == 8: - if data_p[rindex] == data_p[rindex + shapeinfo.Def]: - join_trees(forest_p, rindex, rindex + shapeinfo.Def) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ef]) - if data_p[rindex] == data_p[rindex + shapeinfo.Deg]: - join_trees(forest_p, rindex, rindex + shapeinfo.Deg) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eg]) if x + 1 < shapeinfo.x: - if data_p[rindex] == data_p[rindex + shapeinfo.Deh]: - join_trees(forest_p, rindex, rindex + shapeinfo.Deh) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_eh]) - if data_p[rindex] == data_p[rindex + shapeinfo.Dei]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dei) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ei]) - if data_p[rindex] == data_p[rindex + shapeinfo.Dej]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dej) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ej]) if neighbors == 8: if x + 1 < shapeinfo.x: - if data_p[rindex] == data_p[rindex + shapeinfo.Dek]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dek) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ek]) - if data_p[rindex] == data_p[rindex + shapeinfo.Del]: - join_trees(forest_p, rindex, rindex + shapeinfo.Del) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_el]) - if data_p[rindex] == data_p[rindex + shapeinfo.Dem]: - join_trees(forest_p, rindex, rindex + shapeinfo.Dem) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_em]) if x + 1 < shapeinfo.x: - if data_p[rindex] == data_p[rindex + shapeinfo.Den]: - join_trees(forest_p, rindex, rindex + shapeinfo.Den) + join_trees_wrapper(data_p, forest_p, rindex, DEX[D_en]) From 98047f5ce87939daf39c6c0e692829e1a04167a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20T=C3=BD=C4=8D?= Date: Sun, 26 Oct 2014 23:38:12 +0100 Subject: [PATCH 0608/1122] Cython minor fixes --- skimage/measure/_ccomp.pyx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/measure/_ccomp.pyx b/skimage/measure/_ccomp.pyx index e537b598..03be39d7 100644 --- a/skimage/measure/_ccomp.pyx +++ b/skimage/measure/_ccomp.pyx @@ -63,7 +63,7 @@ cdef struct s_shpinfo: cdef shpinfo get_triple(inarr_shape): - cdef shpinfo res = shpinfo() + cdef shpinfo res res.y = 1 res.z = 1 @@ -389,7 +389,7 @@ cdef DTYPE_t resolve_labels(DTYPE_t * data_p, DTYPE_t * forest_p, # The 1D indices are "raveled" or "linear", that's where "rindex" comes from. -cdef scan1D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, +cdef void scan1D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, bginfo * bg, DTYPE_t neighbors, DTYPE_t y, DTYPE_t z): """ Perform forward scan on a 1D object, usually the first row of an image @@ -412,7 +412,7 @@ cdef scan1D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) -cdef scan2D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, +cdef void scan2D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, bginfo * bg, DTYPE_t neighbors, DTYPE_t z): """ Perform forward scan on a 2D array. @@ -452,7 +452,7 @@ cdef scan2D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) -cdef scan3D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, +cdef void scan3D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, bginfo * bg, DTYPE_t neighbors): """ Perform forward scan on a 2D array. From 152a6d0de84d0ff279d755a4141065f05dafdb06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20T=C3=BD=C4=8D?= Date: Mon, 27 Oct 2014 22:37:04 +0100 Subject: [PATCH 0609/1122] Fixed typos + added docstrings and comments --- skimage/measure/_ccomp.pyx | 61 ++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/skimage/measure/_ccomp.pyx b/skimage/measure/_ccomp.pyx index 03be39d7..be0148cf 100644 --- a/skimage/measure/_ccomp.pyx +++ b/skimage/measure/_ccomp.pyx @@ -31,8 +31,8 @@ ctypedef cnp.int32_t INTS_t cdef struct s_shpinfo -ctypedef s_shpinfo shpinfo -ctypedef int (* fun_ravel)(int, int, int, shpinfo *) +ctypedef s_shpinfo shape_info +ctypedef int (* fun_ravel)(int, int, int, shape_info *) ctypedef struct bginfo: @@ -49,21 +49,33 @@ cdef enum: # Structure for centralised access to shape data +# Contains information related to the shape of the input array cdef struct s_shpinfo: INTS_t x INTS_t y INTS_t z + # Number of elements DTYPE_t numels + # Number of of the input array INTS_t ndim + # Offsets between elements recalculated to linear index increments + # DEX[D_ea] is offset between E and A (i.e. to the point to upper left) + # The name DEX is supposed to evoke DE., where . = A, B, C, D, F etc. INTS_t DEX[D_COUNT] + # Function pointer to a function that recalculates multiindex to linear + # index. Heavily depends on dimensions of the input array. fun_ravel ravel_index -cdef shpinfo get_triple(inarr_shape): - cdef shpinfo res +cdef shape_info get_shape_info(inarr_shape): + """ + Precalculates all the needed data from the input array shape + and stores them in the shape_info struct. + """ + cdef shape_info res res.y = 1 res.z = 1 @@ -131,14 +143,14 @@ cdef inline void join_trees_wrapper(DTYPE_t * data_p, DTYPE_t * forest_p, join_trees(forest_p, rindex, rindex + idxdiff) -cdef int ravel_index1D(int x, int y, int z, shpinfo * shapeinfo): +cdef int ravel_index1D(int x, int y, int z, shape_info * shapeinfo): """ Ravel index of a 1D array - trivial. y and z are ignored. """ return x -cdef int ravel_index2D(int x, int y, int z, shpinfo * shapeinfo): +cdef int ravel_index2D(int x, int y, int z, shape_info * shapeinfo): """ Ravel index of a 2D array. z is ignored """ @@ -146,7 +158,7 @@ cdef int ravel_index2D(int x, int y, int z, shpinfo * shapeinfo): return ret -cdef int ravel_index3D(int x, int y, int z, shpinfo * shapeinfo): +cdef int ravel_index3D(int x, int y, int z, shape_info * shapeinfo): """ Ravel index of a 3D array """ @@ -169,7 +181,7 @@ cdef int ravel_index3D(int x, int y, int z, shpinfo * shapeinfo): # 1 is the root. # Last but not least, one array can hold more than one tree as long as their # indices are different. It is the case in this algorithm, so for that reason -# the array is referred to as the "forrest" = multiple trees next to each +# the array is referred to as the "forest" = multiple trees next to each # other. # # In this algorithm, there are as many indices as there are elements in the @@ -259,6 +271,8 @@ def label(input, DTYPE_t neighbors=8, background=None, return_num=False): Image to label. neighbors : {4, 8}, int, optional Whether to use 4- or 8-connectivity. + In 3D, 4-connectivity means connected pixels share have to share face, + whereas with 8-connectivity, they have to share only edge or vertex. background : int, optional Consider all pixels with this value as background pixels, and label them as -1. (Note: background pixels will be labeled as 0 starting with @@ -309,16 +323,16 @@ def label(input, DTYPE_t neighbors=8, background=None, return_num=False): # Having data a 2D array slows down access considerably using linear # indices even when using the data_p pointer :-( - data = input.flatten().astype(DTYPE, copy=True) + data = np.copy(input.flatten().astype(DTYPE), order="C") forest = np.arange(data.size, dtype=DTYPE) cdef DTYPE_t *forest_p = forest.data cdef DTYPE_t *data_p = data.data - cdef shpinfo shapeinfo + cdef shape_info shapeinfo cdef bginfo bg - shapeinfo = get_triple(input.shape) + shapeinfo = get_shape_info(input.shape) bg.background_val = 0 bg.background_node = -999 @@ -342,7 +356,7 @@ def label(input, DTYPE_t neighbors=8, background=None, return_num=False): scan3D(data_p, forest_p, & shapeinfo, & bg, neighbors) # Label output - cdef DTYPE_t ctr = 0 + cdef DTYPE_t ctr ctr = resolve_labels(data_p, forest_p, & shapeinfo, & bg) # Work around a bug in ndimage's type checking on 32-bit platforms @@ -358,17 +372,20 @@ def label(input, DTYPE_t neighbors=8, background=None, return_num=False): cdef DTYPE_t resolve_labels(DTYPE_t * data_p, DTYPE_t * forest_p, - shpinfo * shapeinfo, bginfo * bg): + shape_info * shapeinfo, bginfo * bg): """ We iterate through the provisional labels and assign final labels based on our knowledge of prov. labels relationship. We also track how many distinct final labels we have. """ - cdef DTYPE_t counter = 0 + cdef DTYPE_t counter = 0, i + for i in range(shapeinfo.numels): if i == bg.background_node: data_p[i] = -1 elif i == forest_p[i]: + # We have stumbled across a root which is something new to us (root + # is the LOWEST of all prov. labels that are equivalent to it) data_p[i] = counter counter += 1 else: @@ -378,9 +395,9 @@ cdef DTYPE_t resolve_labels(DTYPE_t * data_p, DTYPE_t * forest_p, # Here, we work with flat arrays regardless whether the data is 1, 2 or 3D. # The lookup to the neighbor in a 2D array is achieved by precalculating an -# offset and ading it to the index. +# offset and adding it to the index. # The forward scan mask looks like this (the center point is actually E): -# (take a look at shpinfo docs for more exhaustive info) +# (take a look at shape_info docs for more exhaustive info) # A B C # D E # @@ -389,8 +406,8 @@ cdef DTYPE_t resolve_labels(DTYPE_t * data_p, DTYPE_t * forest_p, # The 1D indices are "raveled" or "linear", that's where "rindex" comes from. -cdef void scan1D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, - bginfo * bg, DTYPE_t neighbors, DTYPE_t y, DTYPE_t z): +cdef void scan1D(DTYPE_t * data_p, DTYPE_t * forest_p, shape_info * shapeinfo, + bginfo * bg, DTYPE_t neighbors, DTYPE_t y, DTYPE_t z): """ Perform forward scan on a 1D object, usually the first row of an image """ @@ -412,8 +429,8 @@ cdef void scan1D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) -cdef void scan2D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, - bginfo * bg, DTYPE_t neighbors, DTYPE_t z): +cdef void scan2D(DTYPE_t * data_p, DTYPE_t * forest_p, shape_info * shapeinfo, + bginfo * bg, DTYPE_t neighbors, DTYPE_t z): """ Perform forward scan on a 2D array. """ @@ -452,8 +469,8 @@ cdef void scan2D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) -cdef void scan3D(DTYPE_t * data_p, DTYPE_t * forest_p, shpinfo * shapeinfo, - bginfo * bg, DTYPE_t neighbors): +cdef void scan3D(DTYPE_t * data_p, DTYPE_t * forest_p, shape_info * shapeinfo, + bginfo * bg, DTYPE_t neighbors): """ Perform forward scan on a 2D array. """ From 416f3a2eb55c8815717cb8ba3cd1d78d55fb7250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20T=C3=BD=C4=8D?= Date: Mon, 27 Oct 2014 23:33:59 +0100 Subject: [PATCH 0610/1122] numpy 1.6 compat fix --- skimage/measure/_ccomp.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/measure/_ccomp.pyx b/skimage/measure/_ccomp.pyx index be0148cf..bed57069 100644 --- a/skimage/measure/_ccomp.pyx +++ b/skimage/measure/_ccomp.pyx @@ -323,7 +323,7 @@ def label(input, DTYPE_t neighbors=8, background=None, return_num=False): # Having data a 2D array slows down access considerably using linear # indices even when using the data_p pointer :-( - data = np.copy(input.flatten().astype(DTYPE), order="C") + data = np.copy(input.flatten().astype(DTYPE)) forest = np.arange(data.size, dtype=DTYPE) cdef DTYPE_t *forest_p = forest.data From 815ea29d26dad62f2e88620f2127ca22f42580e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20T=C3=BD=C4=8D?= Date: Wed, 29 Oct 2014 00:58:01 +0100 Subject: [PATCH 0611/1122] Added some comments + requested reformatting --- skimage/measure/_ccomp.pyx | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/skimage/measure/_ccomp.pyx b/skimage/measure/_ccomp.pyx index bed57069..cc4a5320 100644 --- a/skimage/measure/_ccomp.pyx +++ b/skimage/measure/_ccomp.pyx @@ -35,15 +35,22 @@ ctypedef s_shpinfo shape_info ctypedef int (* fun_ravel)(int, int, int, shape_info *) +# For having stuff concerning background in one place ctypedef struct bginfo: DTYPE_t background_val DTYPE_t background_node +# A pixel has neighbors that have already been scanned. +# In the paper, the pixel is denoted by E and its neighbors +# by A, B, C and D (in 2D) - see doc for function get_shape_info() cdef enum: # D_ee, # We don't need D_ee + # the 1D neighbor D_ed, + # 2D neighbors D_ea, D_eb, D_ec, + # 3D neighbors D_ef, D_eg, D_eh, D_ei, D_ej, D_ek, D_el, D_em, D_en, D_COUNT @@ -137,20 +144,20 @@ cdef shape_info get_shape_info(inarr_shape): return res -cdef inline void join_trees_wrapper(DTYPE_t * data_p, DTYPE_t * forest_p, +cdef inline void join_trees_wrapper(DTYPE_t *data_p, DTYPE_t *forest_p, DTYPE_t rindex, INTS_t idxdiff): if data_p[rindex] == data_p[rindex + idxdiff]: join_trees(forest_p, rindex, rindex + idxdiff) -cdef int ravel_index1D(int x, int y, int z, shape_info * shapeinfo): +cdef int ravel_index1D(int x, int y, int z, shape_info *shapeinfo): """ Ravel index of a 1D array - trivial. y and z are ignored. """ return x -cdef int ravel_index2D(int x, int y, int z, shape_info * shapeinfo): +cdef int ravel_index2D(int x, int y, int z, shape_info *shapeinfo): """ Ravel index of a 2D array. z is ignored """ @@ -158,7 +165,7 @@ cdef int ravel_index2D(int x, int y, int z, shape_info * shapeinfo): return ret -cdef int ravel_index3D(int x, int y, int z, shape_info * shapeinfo): +cdef int ravel_index3D(int x, int y, int z, shape_info *shapeinfo): """ Ravel index of a 3D array """ @@ -371,8 +378,8 @@ def label(input, DTYPE_t neighbors=8, background=None, return_num=False): return res -cdef DTYPE_t resolve_labels(DTYPE_t * data_p, DTYPE_t * forest_p, - shape_info * shapeinfo, bginfo * bg): +cdef DTYPE_t resolve_labels(DTYPE_t *data_p, DTYPE_t *forest_p, + shape_info *shapeinfo, bginfo *bg): """ We iterate through the provisional labels and assign final labels based on our knowledge of prov. labels relationship. @@ -406,14 +413,14 @@ cdef DTYPE_t resolve_labels(DTYPE_t * data_p, DTYPE_t * forest_p, # The 1D indices are "raveled" or "linear", that's where "rindex" comes from. -cdef void scan1D(DTYPE_t * data_p, DTYPE_t * forest_p, shape_info * shapeinfo, - bginfo * bg, DTYPE_t neighbors, DTYPE_t y, DTYPE_t z): +cdef void scan1D(DTYPE_t *data_p, DTYPE_t *forest_p, shape_info *shapeinfo, + bginfo *bg, DTYPE_t neighbors, DTYPE_t y, DTYPE_t z): """ Perform forward scan on a 1D object, usually the first row of an image """ # Initialize the first row cdef DTYPE_t x, rindex - cdef INTS_t * DEX = shapeinfo.DEX + cdef INTS_t *DEX = shapeinfo.DEX rindex = shapeinfo.ravel_index(0, y, z, shapeinfo) if data_p[rindex] == bg.background_val: @@ -429,13 +436,13 @@ cdef void scan1D(DTYPE_t * data_p, DTYPE_t * forest_p, shape_info * shapeinfo, join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) -cdef void scan2D(DTYPE_t * data_p, DTYPE_t * forest_p, shape_info * shapeinfo, - bginfo * bg, DTYPE_t neighbors, DTYPE_t z): +cdef void scan2D(DTYPE_t *data_p, DTYPE_t *forest_p, shape_info *shapeinfo, + bginfo *bg, DTYPE_t neighbors, DTYPE_t z): """ Perform forward scan on a 2D array. """ cdef DTYPE_t x, y, rindex - cdef INTS_t * DEX = shapeinfo.DEX + cdef INTS_t *DEX = shapeinfo.DEX scan1D(data_p, forest_p, shapeinfo, bg, neighbors, 0, z) for y in range(1, shapeinfo.y): rindex = shapeinfo.ravel_index(0, y, 0, shapeinfo) @@ -469,13 +476,13 @@ cdef void scan2D(DTYPE_t * data_p, DTYPE_t * forest_p, shape_info * shapeinfo, join_trees_wrapper(data_p, forest_p, rindex, DEX[D_ed]) -cdef void scan3D(DTYPE_t * data_p, DTYPE_t * forest_p, shape_info * shapeinfo, - bginfo * bg, DTYPE_t neighbors): +cdef void scan3D(DTYPE_t *data_p, DTYPE_t *forest_p, shape_info *shapeinfo, + bginfo *bg, DTYPE_t neighbors): """ Perform forward scan on a 2D array. """ cdef DTYPE_t x, y, z, rindex - cdef INTS_t * DEX = shapeinfo.DEX + cdef INTS_t *DEX = shapeinfo.DEX # Handle first plane scan2D(data_p, forest_p, shapeinfo, bg, neighbors, 0) for z in range(1, shapeinfo.z): From 99818f02ebc46debe349a6c1b6bba70be6e04968 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Wed, 29 Oct 2014 13:38:40 +0200 Subject: [PATCH 0612/1122] Update error message for no plugins --- skimage/io/_plugins/null_plugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/io/_plugins/null_plugin.py b/skimage/io/_plugins/null_plugin.py index 4eb7adf3..28559701 100644 --- a/skimage/io/_plugins/null_plugin.py +++ b/skimage/io/_plugins/null_plugin.py @@ -3,11 +3,11 @@ __all__ = ['imshow', 'imread', 'imsave', '_app_show'] import warnings message = '''\ -No plugin has been loaded. Please refer to +No plugin has been loaded. Please refer to the docstring for ``skimage.io`` +for a list of available plugins. You may specify a plugin explicitly as +an argument to ``imread``, e.g. ``imread("image.jpg", plugin='pil')``. -skimage.io.plugins() - -for a list of available plugins.''' +''' def imshow(*args, **kwargs): From 92929fc4172e1be88271c188146934e6dda33c3d Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Wed, 29 Oct 2014 13:39:42 +0200 Subject: [PATCH 0613/1122] Print correct version when dependency is not satisfied --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b4b74ab3..1d7b3d1c 100755 --- a/setup.py +++ b/setup.py @@ -103,7 +103,7 @@ def check_requirements(): dep_error = True if dep_error: raise ImportError('You need `%s` version %s or later.' \ - % (package_name, '.'.join(str(i) for i in min_version))) + % (package_name, min_version)) if __name__ == "__main__": From 86fdbf460f6bb08f69f90aa4aa2288536ff28b34 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 30 Oct 2014 20:43:16 -0500 Subject: [PATCH 0614/1122] Use recommended extern import method for cython --- skimage/restoration/_unwrap_2d.pyx | 11 ++++++----- skimage/restoration/_unwrap_3d.pyx | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/skimage/restoration/_unwrap_2d.pyx b/skimage/restoration/_unwrap_2d.pyx index 6e889729..6e486439 100644 --- a/skimage/restoration/_unwrap_2d.pyx +++ b/skimage/restoration/_unwrap_2d.pyx @@ -1,8 +1,9 @@ -cdef extern void unwrap2D(double* wrapped_image, - double* unwrapped_image, - unsigned char* input_mask, - int image_width, int image_height, - int wrap_around_x, int wrap_around_y) +cdef extern from *: + void unwrap2D(double* wrapped_image, + double* unwrapped_image, + unsigned char* input_mask, + int image_width, int image_height, + int wrap_around_x, int wrap_around_y) def unwrap_2d(double[:, ::1] image, unsigned char[:, ::1] mask, diff --git a/skimage/restoration/_unwrap_3d.pyx b/skimage/restoration/_unwrap_3d.pyx index 370d58be..3a1869a8 100644 --- a/skimage/restoration/_unwrap_3d.pyx +++ b/skimage/restoration/_unwrap_3d.pyx @@ -1,8 +1,9 @@ -cdef extern void unwrap3D(double* wrapped_volume, - double* unwrapped_volume, - unsigned char* input_mask, - int image_width, int image_height, int volume_depth, - int wrap_around_x, int wrap_around_y, int wrap_around_z) +cdef extern from *: + void unwrap3D(double* wrapped_volume, + double* unwrapped_volume, + unsigned char* input_mask, + int image_width, int image_height, int volume_depth, + int wrap_around_x, int wrap_around_y, int wrap_around_z) def unwrap_3d(double[:, :, ::1] image, unsigned char[:, :, ::1] mask, From 21d6ef15ecee0e8913648533104f6c0ad50e9eb9 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 2 Nov 2014 06:28:17 -0600 Subject: [PATCH 0615/1122] Remove null plugin since we have the pil plugin as a requirement --- skimage/io/_plugins/null_plugin.ini | 3 -- skimage/io/_plugins/null_plugin.py | 25 --------------- skimage/io/manage_plugins.py | 2 +- skimage/io/tests/test_null.py | 50 ----------------------------- 4 files changed, 1 insertion(+), 79 deletions(-) delete mode 100644 skimage/io/_plugins/null_plugin.ini delete mode 100644 skimage/io/_plugins/null_plugin.py delete mode 100644 skimage/io/tests/test_null.py diff --git a/skimage/io/_plugins/null_plugin.ini b/skimage/io/_plugins/null_plugin.ini deleted file mode 100644 index 9703aab0..00000000 --- a/skimage/io/_plugins/null_plugin.ini +++ /dev/null @@ -1,3 +0,0 @@ -[null] -description = Default plugin that does nothing -provides = imshow, imread, imsave, _app_show diff --git a/skimage/io/_plugins/null_plugin.py b/skimage/io/_plugins/null_plugin.py deleted file mode 100644 index 28559701..00000000 --- a/skimage/io/_plugins/null_plugin.py +++ /dev/null @@ -1,25 +0,0 @@ -__all__ = ['imshow', 'imread', 'imsave', '_app_show'] - -import warnings - -message = '''\ -No plugin has been loaded. Please refer to the docstring for ``skimage.io`` -for a list of available plugins. You may specify a plugin explicitly as -an argument to ``imread``, e.g. ``imread("image.jpg", plugin='pil')``. - -''' - - -def imshow(*args, **kwargs): - warnings.warn(RuntimeWarning(message)) - - -def imread(*args, **kwargs): - warnings.warn(RuntimeWarning(message)) - - -def imsave(*args, **kwargs): - warnings.warn(RuntimeWarning(message)) - - -_app_show = imshow diff --git a/skimage/io/manage_plugins.py b/skimage/io/manage_plugins.py index c9e96f8e..9bd95cb7 100644 --- a/skimage/io/manage_plugins.py +++ b/skimage/io/manage_plugins.py @@ -44,7 +44,7 @@ plugin_meta_data = {} # the following preferences. preferred_plugins = { # Default plugins for all types (overridden by specific types below). - 'all': ['pil', 'matplotlib', 'qt', 'freeimage', 'null'] + 'all': ['pil', 'matplotlib', 'qt', 'freeimage'] } diff --git a/skimage/io/tests/test_null.py b/skimage/io/tests/test_null.py deleted file mode 100644 index 56f5df89..00000000 --- a/skimage/io/tests/test_null.py +++ /dev/null @@ -1,50 +0,0 @@ -import os -import warnings -from contextlib import contextmanager - -import numpy as np -from numpy.testing import raises - -from skimage import io -from skimage import data_dir - - -@contextmanager -def warnings_as_errors(): - # Temporarily set warnings as errors so we can test the warning is raised. - with warnings.catch_warnings(): - warnings.filterwarnings('error') - yield - -@raises(Warning) -def test_null_imread(): - path = os.path.join(data_dir, 'color.png') - with warnings_as_errors(): - io.imread(path, plugin='null') - - -@raises(Warning) -def test_null_imsave(): - with warnings_as_errors(): - io.imsave('dummy.png', np.zeros((3, 3)), plugin='null') - - -@raises(Warning) -def test_null_imshow(): - with warnings_as_errors(): - io.imshow(np.zeros((3, 3)), plugin='null') - - -@raises(Warning) -def test_null_imread_collection(): - # Note that the null plugin doesn't define an `imread_collection` plugin - # but this function is dynamically added by the plugin manager. - path = os.path.join(data_dir, '*.png') - with warnings_as_errors(): - collection = io.imread_collection(path, plugin='null') - collection[0] - - -if __name__ == '__main__': - from numpy.testing import run_module_suite - run_module_suite() From 91266c570227841cca5e54c03a3e6b7466e8e53f Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 2 Nov 2014 07:03:01 -0600 Subject: [PATCH 0616/1122] Update tests to use default plugins. --- skimage/io/tests/test_plugin.py | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/skimage/io/tests/test_plugin.py b/skimage/io/tests/test_plugin.py index 06f3a600..00128ab5 100644 --- a/skimage/io/tests/test_plugin.py +++ b/skimage/io/tests/test_plugin.py @@ -4,21 +4,10 @@ from numpy.testing import assert_equal, raises from skimage import io from skimage.io import manage_plugins -from numpy.testing.decorators import skipif -try: - io.use_plugin('pil') - PIL_available = True - priority_plugin = 'pil' -except ImportError: - PIL_available = False -try: - io.use_plugin('freeimage') - FI_available = True - priority_plugin = 'freeimage' -except RuntimeError: - FI_available = False +io.use_plugin('pil') +priority_plugin = 'pil' def setup_module(): @@ -65,7 +54,6 @@ def test_failed_use(): manage_plugins.use_plugin('asd') -@skipif(not PIL_available and not FI_available) def test_use_priority(): manage_plugins.use_plugin(priority_plugin) plug, func = manage_plugins.plugin_store['imread'][0] @@ -76,7 +64,6 @@ def test_use_priority(): assert_equal(plug, 'test') -@skipif(not PIL_available) def test_use_priority_with_func(): manage_plugins.use_plugin('pil') plug, func = manage_plugins.plugin_store['imread'][0] @@ -106,28 +93,28 @@ def test_available(): def test_load_preferred_plugins_all(): - from skimage.io._plugins import null_plugin + from skimage.io._plugins import pil_plugin with protect_preferred_plugins(): - manage_plugins.preferred_plugins = {'all': ['null']} + manage_plugins.preferred_plugins = {'all': ['pil']} manage_plugins.reset_plugins() for plugin_type in ('imread', 'imsave', 'imshow'): plug, func = manage_plugins.plugin_store[plugin_type][0] - assert func == getattr(null_plugin, plugin_type) + assert func == getattr(pil_plugin, plugin_type) def test_load_preferred_plugins_imread(): - from skimage.io._plugins import null_plugin + from skimage.io._plugins import pil_plugin, matplotlib_plugin with protect_preferred_plugins(): - manage_plugins.preferred_plugins['imread'] = ['null'] + manage_plugins.preferred_plugins['imread'] = ['pil'] manage_plugins.reset_plugins() plug, func = manage_plugins.plugin_store['imread'][0] - assert func == null_plugin.imread + assert func == pil_plugin.imread plug, func = manage_plugins.plugin_store['imshow'][0] - assert func != null_plugin.imshow + assert func != matplotlib_plugin.imshow if __name__ == "__main__": From dfffd07e4a353c5ebd3644c98ca1a120852331dd Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 2 Nov 2014 07:03:15 -0600 Subject: [PATCH 0617/1122] Clean up the imports in the pil_plugin --- skimage/io/_plugins/pil_plugin.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index fcc26e9e..e1390685 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -1,20 +1,12 @@ __all__ = ['imread', 'imsave'] import numpy as np - -try: - from PIL import Image -except ImportError: - raise ImportError("The Python Image Library could not be found. " - "Please refer to " - "https://pypi.python.org/pypi/Pillow/ (or " - "http://pypi.python.org/pypi/PIL/) " - "for further instructions.") +from six import string_types +from PIL import Image from skimage.util import img_as_ubyte, img_as_uint - -from six import string_types -from skimage.external.tifffile import imread as tif_imread, imsave as tif_imsave +from skimage.external.tifffile import ( + imread as tif_imread, imsave as tif_imsave) def imread(fname, dtype=None): From 6906dbc9575a2893e7f8d24a1566e3e4e336f3ec Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 2 Nov 2014 07:08:43 -0600 Subject: [PATCH 0618/1122] Use matplotlib as the default imshow --- skimage/io/manage_plugins.py | 3 ++- skimage/io/tests/test_plugin.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/skimage/io/manage_plugins.py b/skimage/io/manage_plugins.py index 9bd95cb7..5932e63d 100644 --- a/skimage/io/manage_plugins.py +++ b/skimage/io/manage_plugins.py @@ -44,7 +44,8 @@ plugin_meta_data = {} # the following preferences. preferred_plugins = { # Default plugins for all types (overridden by specific types below). - 'all': ['pil', 'matplotlib', 'qt', 'freeimage'] + 'all': ['pil', 'matplotlib', 'qt', 'freeimage'], + 'imshow': ['matplotlib'] } diff --git a/skimage/io/tests/test_plugin.py b/skimage/io/tests/test_plugin.py index 00128ab5..117e5ab9 100644 --- a/skimage/io/tests/test_plugin.py +++ b/skimage/io/tests/test_plugin.py @@ -114,7 +114,7 @@ def test_load_preferred_plugins_imread(): plug, func = manage_plugins.plugin_store['imread'][0] assert func == pil_plugin.imread plug, func = manage_plugins.plugin_store['imshow'][0] - assert func != matplotlib_plugin.imshow + assert func == matplotlib_plugin.imshow, func.__module__ if __name__ == "__main__": From d7fdfebb96830b98b67efa57e19a935fd0df1c1d Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 4 Nov 2014 15:08:38 +1100 Subject: [PATCH 0619/1122] Remove contiguous conversion for out array --- skimage/morphology/cmorph.pyx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/skimage/morphology/cmorph.pyx b/skimage/morphology/cmorph.pyx index 9457dd9e..d248ea9c 100644 --- a/skimage/morphology/cmorph.pyx +++ b/skimage/morphology/cmorph.pyx @@ -49,8 +49,6 @@ def _dilate(np.uint8_t[:, :] image, image = np.ascontiguousarray(image) if out is None: out = np.zeros((rows, cols), dtype=np.uint8) - else: - out = np.ascontiguousarray(out) cdef Py_ssize_t r, c, rr, cc, s, value, local_max @@ -125,8 +123,6 @@ def _erode(np.uint8_t[:, :] image, image = np.ascontiguousarray(image) if out is None: out = np.zeros((rows, cols), dtype=np.uint8) - else: - out = np.ascontiguousarray(out) cdef int r, c, rr, cc, s, value, local_min From 1a3ec0299c23eb4f2768baaa1b3168b51a22758d Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 4 Nov 2014 15:20:21 +1100 Subject: [PATCH 0620/1122] Add tests for discontiguous out arrays in grey morpho --- skimage/morphology/tests/test_grey.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index 83d10b45..00f4ad37 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -6,7 +6,6 @@ from scipy import ndimage import skimage from skimage import data_dir -from skimage.util import img_as_bool from skimage.morphology import grey, selem @@ -247,5 +246,27 @@ def test_inplace(): testing.assert_raises(NotImplementedError, f, image, selem, out=out) +def test_discontiguous_out_array(): + image = np.array([[5, 6, 2], + [7, 2, 2], + [3, 5, 1]], np.uint8) + out_array_big = np.zeros((5, 5), np.uint8) + out_array = out_array_big[::2, ::2] + expected_dilation = np.array([[7, 0, 6, 0, 6], + [0, 0, 0, 0, 0], + [7, 0, 7, 0, 2], + [0, 0, 0, 0, 0], + [7, 0, 5, 0, 5]], np.uint8) + expected_erosion = np.array([[5, 0, 2, 0, 2], + [0, 0, 0, 0, 0], + [2, 0, 2, 0, 1], + [0, 0, 0, 0, 0], + [3, 0, 1, 0, 1]], np.uint8) + grey.dilation(image, out=out_array) + testing.assert_array_equal(out_array_big, expected_dilation) + grey.erosion(image, out=out_array) + testing.assert_array_equal(out_array_big, expected_erosion) + + if __name__ == '__main__': testing.run_module_suite() From f52f2c1779b82aac79055850b8a999e4b8d71e49 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 6 Nov 2014 10:27:43 +1100 Subject: [PATCH 0621/1122] Raise meaningful error in reconstruct w bad method If a method other than 'erosion' or 'dilation' was passed, an `UnboundLocalVariable` error for `pad_value` was raised, which made it difficult to determine the source of the error. Now, an eloquent `ValueError` is raised instead. See #1218. --- skimage/morphology/greyreconstruct.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index bce26d6b..ec7aa182 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -152,6 +152,9 @@ def reconstruction(seed, mask, method='dilation', selem=None, offset=None): pad_value = np.min(seed) elif method == 'erosion': pad_value = np.max(seed) + else: + raise ValueError("Reconstruction method can be one of 'erosion' " + "or 'dilation'. Got '%s'." % method) images = np.ones(dims) * pad_value images[[0] + inside_slices] = seed images[[1] + inside_slices] = mask From 39d042e4ba158802530f05d73b74f38751f74f43 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 6 Nov 2014 10:29:43 +1100 Subject: [PATCH 0622/1122] Test correct error for wrong method in reconstruct --- skimage/morphology/tests/test_reconstruction.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skimage/morphology/tests/test_reconstruction.py b/skimage/morphology/tests/test_reconstruction.py index f5678c08..df9ddf8f 100644 --- a/skimage/morphology/tests/test_reconstruction.py +++ b/skimage/morphology/tests/test_reconstruction.py @@ -97,6 +97,12 @@ def test_invalid_selem(): reconstruction(seed, mask, selem=np.ones((3, 3))) +def test_invalid_method(): + seed = np.array([0, 8, 8, 8, 8, 8, 8, 8, 8, 0]) + mask = np.array([0, 3, 6, 2, 1, 1, 1, 4, 2, 0]) + assert_raises(ValueError, reconstruction, seed, mask, method='foo') + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() From 25653dc28822bb40d55cca43f73762e31a31782c Mon Sep 17 00:00:00 2001 From: Jonathan Helmus Date: Thu, 6 Nov 2014 21:03:14 -0600 Subject: [PATCH 0623/1122] TST: Unit test to check for 3d unwrap seg fault Added a unit test to check for bug in unwrap_phase which causes a segmentation fault when the middle dimension is connected. --- skimage/restoration/tests/test_unwrap.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/skimage/restoration/tests/test_unwrap.py b/skimage/restoration/tests/test_unwrap.py index fca63724..764e8b74 100644 --- a/skimage/restoration/tests/test_unwrap.py +++ b/skimage/restoration/tests/test_unwrap.py @@ -144,5 +144,12 @@ def test_invalid_input(): assert_raises(ValueError, unwrap_phase, np.zeros((1, 1)), 'False') +def test_unwrap_3d_middle_wrap_around(): + # Segmentation fault in 3D unwrap phase with middle dimension connected + # GitHub issue #1171 + image = np.zeros((20, 30, 40), dtype=np.float32) + unwrap = unwrap_phase(image, wrap_around=[False, True, False]) + assert np.all(unwrap == 0) + if __name__ == "__main__": run_module_suite() From e0e4fe26f14184890a8a54195f3ed8d6ae8a5b32 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 19:08:19 -0500 Subject: [PATCH 0624/1122] Add tifffile support for MultiImage --- skimage/io/collection.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index 83811fc0..708f0918 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -10,6 +10,8 @@ from copy import copy import numpy as np import six +from tifffile import Tifffile + __all__ = ['MultiImage', 'ImageCollection', 'concatenate_images', 'imread_collection_wrapper'] @@ -113,8 +115,13 @@ class MultiImage(object): self._dtype = dtype self._cached = None - from PIL import Image - img = Image.open(self._filename) + if filename.lower.endswith(('.tiff', '.tif')): + self.tif_img = Tifffile(self._filename) + + else: + from PIL import Image + img = Image.open(self._filename) + self.tif_img = None if self._conserve_memory: self._numframes = self._find_numframes(img) else: @@ -131,6 +138,8 @@ class MultiImage(object): def _find_numframes(self, img): """Find the number of frames in the multi-img.""" + if self._tif_img: + return len(img.pages) i = 0 while True: i += 1 @@ -142,6 +151,8 @@ class MultiImage(object): def _getframe(self, framenum): """Open the image and extract the frame.""" + if self._tif_img: + return self._tif_img[framenum].asarray() from PIL import Image img = Image.open(self.filename) img.seek(framenum) @@ -149,6 +160,8 @@ class MultiImage(object): def _getallframes(self, img): """Extract all frames from the multi-img.""" + if self._tif_img: + return [self._tif_img[i].asarray() for i in self._tif_img.pages] frames = [] try: i = 0 From 0134946fc73f8b166f9464a438863fb8073d75ff Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 19:19:54 -0500 Subject: [PATCH 0625/1122] Update multi_image test and install tifffile for travis. --- skimage/io/tests/test_multi_image.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/skimage/io/tests/test_multi_image.py b/skimage/io/tests/test_multi_image.py index ebaa71dc..be9aef65 100644 --- a/skimage/io/tests/test_multi_image.py +++ b/skimage/io/tests/test_multi_image.py @@ -1,19 +1,11 @@ import os import numpy as np -from numpy.testing.decorators import skipif from numpy.testing import assert_raises, assert_equal, assert_allclose from skimage import data_dir from skimage.io.collection import MultiImage -try: - from PIL import Image -except ImportError: - PIL_available = False -else: - PIL_available = True - import six @@ -22,14 +14,11 @@ class TestMultiImage(): def setUp(self): # This multipage TIF file was created with imagemagick: # convert im1.tif im2.tif -adjoin multipage.tif - if PIL_available: - self.img = MultiImage(os.path.join(data_dir, 'multipage.tif')) + self.img = MultiImage(os.path.join(data_dir, 'multipage.tif')) - @skipif(not PIL_available) def test_len(self): assert len(self.img) == 2 - @skipif(not PIL_available) def test_getitem(self): num = len(self.img) for i in range(-num, num): @@ -42,7 +31,6 @@ class TestMultiImage(): assert_raises(IndexError, return_img, num) assert_raises(IndexError, return_img, -num - 1) - @skipif(not PIL_available) def test_files_property(self): assert isinstance(self.img.filename, six.string_types) @@ -50,7 +38,6 @@ class TestMultiImage(): self.img.filename = f assert_raises(AttributeError, set_filename, 'newfile') - @skipif(not PIL_available) def test_conserve_memory_property(self): assert isinstance(self.img.conserve_memory, bool) @@ -58,7 +45,6 @@ class TestMultiImage(): self.img.conserve_memory = val assert_raises(AttributeError, set_mem, True) - @skipif(not PIL_available) def test_concatenate(self): array = self.img.concatenate() assert_equal(array.shape, (len(self.img),) + self.img[0].shape) From a70a4933aa3ef83900d154d5f29b756ec783062e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 19:59:22 -0500 Subject: [PATCH 0626/1122] Fix implementation errors --- skimage/io/collection.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index 708f0918..cb5cc908 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -10,7 +10,7 @@ from copy import copy import numpy as np import six -from tifffile import Tifffile +from tifffile import TiffFile __all__ = ['MultiImage', 'ImageCollection', 'concatenate_images', @@ -115,8 +115,8 @@ class MultiImage(object): self._dtype = dtype self._cached = None - if filename.lower.endswith(('.tiff', '.tif')): - self.tif_img = Tifffile(self._filename) + if filename.lower().endswith(('.tiff', '.tif')): + self.tif_img = img = TiffFile(self._filename) else: from PIL import Image @@ -138,7 +138,7 @@ class MultiImage(object): def _find_numframes(self, img): """Find the number of frames in the multi-img.""" - if self._tif_img: + if self.tif_img: return len(img.pages) i = 0 while True: @@ -151,8 +151,8 @@ class MultiImage(object): def _getframe(self, framenum): """Open the image and extract the frame.""" - if self._tif_img: - return self._tif_img[framenum].asarray() + if self.tif_img: + return self.tif_img[framenum].asarray() from PIL import Image img = Image.open(self.filename) img.seek(framenum) @@ -160,8 +160,8 @@ class MultiImage(object): def _getallframes(self, img): """Extract all frames from the multi-img.""" - if self._tif_img: - return [self._tif_img[i].asarray() for i in self._tif_img.pages] + if self.tif_img: + return [self.tif_img[i].asarray() for i in self.tif_img.pages] frames = [] try: i = 0 From 732c9c37ccb269c2c92f568c304e700b70b80d5d Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 19:08:19 -0500 Subject: [PATCH 0627/1122] Add tifffile support for MultiImage --- skimage/io/collection.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index cb5cc908..ab445214 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -115,6 +115,7 @@ class MultiImage(object): self._dtype = dtype self._cached = None + if filename.lower().endswith(('.tiff', '.tif')): self.tif_img = img = TiffFile(self._filename) @@ -138,7 +139,11 @@ class MultiImage(object): def _find_numframes(self, img): """Find the number of frames in the multi-img.""" +<<<<<<< HEAD if self.tif_img: +======= + if self._tif_img: +>>>>>>> 70b6909... Add tifffile support for MultiImage return len(img.pages) i = 0 while True: @@ -151,8 +156,8 @@ class MultiImage(object): def _getframe(self, framenum): """Open the image and extract the frame.""" - if self.tif_img: - return self.tif_img[framenum].asarray() + if self._tif_img: + return self._tif_img[framenum].asarray() from PIL import Image img = Image.open(self.filename) img.seek(framenum) From 09e6439310c2fcc88ef065ed796cd4415ac4274c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 19:19:54 -0500 Subject: [PATCH 0628/1122] Update multi_image test and install tifffile for travis. --- skimage/io/collection.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index ab445214..9d12ef01 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -115,7 +115,7 @@ class MultiImage(object): self._dtype = dtype self._cached = None - + if filename.lower().endswith(('.tiff', '.tif')): self.tif_img = img = TiffFile(self._filename) @@ -139,11 +139,8 @@ class MultiImage(object): def _find_numframes(self, img): """Find the number of frames in the multi-img.""" -<<<<<<< HEAD + if self.tif_img: -======= - if self._tif_img: ->>>>>>> 70b6909... Add tifffile support for MultiImage return len(img.pages) i = 0 while True: From caaf37ec0c218aa4dfdaa5a96295a81300e335c1 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 19:59:22 -0500 Subject: [PATCH 0629/1122] Fix implementation errors --- skimage/io/collection.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index 9d12ef01..acdbe0c7 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -139,7 +139,6 @@ class MultiImage(object): def _find_numframes(self, img): """Find the number of frames in the multi-img.""" - if self.tif_img: return len(img.pages) i = 0 @@ -153,8 +152,8 @@ class MultiImage(object): def _getframe(self, framenum): """Open the image and extract the frame.""" - if self._tif_img: - return self._tif_img[framenum].asarray() + if self.tif_img: + return self.tif_img[framenum].asarray() from PIL import Image img = Image.open(self.filename) img.seek(framenum) From 49a92482cb650a7c1a7a4771c5f1bd9bd0d4456c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 20 Oct 2014 13:48:40 -0500 Subject: [PATCH 0630/1122] Cleanup --- skimage/io/collection.py | 52 ++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index acdbe0c7..ad2bbc44 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -9,8 +9,9 @@ from copy import copy import numpy as np import six +from PIL import Image -from tifffile import TiffFile +from skimage.external.tifffile import TiffFile __all__ = ['MultiImage', 'ImageCollection', 'concatenate_images', @@ -120,11 +121,12 @@ class MultiImage(object): self.tif_img = img = TiffFile(self._filename) else: - from PIL import Image img = Image.open(self._filename) self.tif_img = None + if self._conserve_memory: self._numframes = self._find_numframes(img) + else: self._frames = self._getallframes(img) self._numframes = len(self._frames) @@ -141,37 +143,41 @@ class MultiImage(object): """Find the number of frames in the multi-img.""" if self.tif_img: return len(img.pages) - i = 0 - while True: - i += 1 - try: - img.seek(i) - except EOFError: - break - return i + + else: + i = 0 + while True: + i += 1 + try: + img.seek(i) + except EOFError: + break + return i def _getframe(self, framenum): """Open the image and extract the frame.""" if self.tif_img: return self.tif_img[framenum].asarray() - from PIL import Image - img = Image.open(self.filename) - img.seek(framenum) - return np.asarray(img, dtype=self._dtype) + + else: + img = Image.open(self.filename) + img.seek(framenum) + return np.asarray(img, dtype=self._dtype) def _getallframes(self, img): """Extract all frames from the multi-img.""" if self.tif_img: return [self.tif_img[i].asarray() for i in self.tif_img.pages] - frames = [] - try: - i = 0 - while True: - frames.append(np.asarray(img, dtype=self._dtype)) - i += 1 - img.seek(i) - except EOFError: - return frames + else: + frames = [] + try: + i = 0 + while True: + frames.append(np.asarray(img, dtype=self._dtype)) + i += 1 + img.seek(i) + except EOFError: + return frames def __getitem__(self, n): """Return the n-th frame as an array. From 945e1638670e917a3a04900bdf11b93a9448e869 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 20 Oct 2014 21:24:19 -0500 Subject: [PATCH 0631/1122] Add animated gif, a test for it, and tests without conserve_memory. Add animated gif, and a test for it, plus tests without conserve_memory Add the gif file --- skimage/data/no_time_for_that.gif | Bin 0 -> 908551 bytes skimage/io/collection.py | 12 +++++++++--- skimage/io/tests/test_multi_image.py | 27 +++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 skimage/data/no_time_for_that.gif diff --git a/skimage/data/no_time_for_that.gif b/skimage/data/no_time_for_that.gif new file mode 100644 index 0000000000000000000000000000000000000000..45607b9560fde1ae4b583f8f2950ebca4c36b271 GIT binary patch literal 908551 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%0Dks+LkeOLoOqKUcR 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