diff --git a/.travis.yml b/.travis.yml index a46b7171..14dd7c30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ python: - 3.4 before_install: - - sudo apt-get update + - sudo apt-get update; true - source tools/travis_before_install.sh - which python; python --version diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index 58b83296..0fa66ddd 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -19,8 +19,8 @@ Here's the long and short of it: * Now, you have remote repositories named: - - ``upstream``, which refers to the ``scikit-image`` repository - - ``origin``, which refers to your personal fork + - ``upstream``, which refers to the ``scikit-image`` repository + - ``origin``, which refers to your personal fork 2. Develop your contribution: @@ -56,32 +56,32 @@ For a more detailed discussion, read these :doc:`detailed documents 4. Review process: - * Reviewers (the other developers and interested community members) will - write inline and/or general comments on your Pull Request (PR) to help - you improve its implementation, documentation and style. Every single - developer working on the project has their code reviewed, and we've come - to see it as friendly conversation from which we all learn and the - overall code quality benefits. Therefore, please don't let the review - discourage you from contributing: its only aim is to improve the quality - of project, not to criticize (we are, after all, very grateful for the - time you're donating!). + * Reviewers (the other developers and interested community members) will + write inline and/or general comments on your Pull Request (PR) to help + you improve its implementation, documentation and style. Every single + developer working on the project has their code reviewed, and we've come + to see it as friendly conversation from which we all learn and the + overall code quality benefits. Therefore, please don't let the review + discourage you from contributing: its only aim is to improve the quality + of project, not to criticize (we are, after all, very grateful for the + time you're donating!). - * To update your pull request, make your changes on your local repository - and commit. As soon as those changes are pushed up (to the same branch as - before) the pull request will update automatically. + * To update your pull request, make your changes on your local repository + and commit. As soon as those changes are pushed up (to the same branch as + before) the pull request will update automatically. - * `Travis-CI `__, a continuous integration service, - is triggered after each Pull Request update to build the code, run unit - tests, measure code coverage and check coding style (PEP8) of your - branch. The Travis tests must pass before your PR can be merged. If - Travis fails, you can find out why by clicking on the "failed" icon (red - cross) and inspecting the build and test log. + * `Travis-CI `__, a continuous integration service, + is triggered after each Pull Request update to build the code, run unit + tests, measure code coverage and check coding style (PEP8) of your + branch. The Travis tests must pass before your PR can be merged. If + 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``. + 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:: @@ -191,10 +191,11 @@ Travis-CI checks all unittests in the project to prevent breakage. Before sending a pull request, you may want to check that Travis-CI successfully passes all tests. To do so, - * Go to `Travis-CI `__ and follow the Sign In link at the top +* Go to `Travis-CI `__ and follow the Sign In link at + the top - * Go to your `profile page `__ and switch - on your scikit-image fork +* Go to your `profile page `__ and switch on + your scikit-image fork It corresponds to steps one and two in `Travis-CI documentation `__ diff --git a/DEPENDS.txt b/DEPENDS.txt index 5cf37564..c7f91df5 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -45,11 +45,11 @@ functionality is only available with the following installed: * `Astropy `__ provides FITS io capability. -* `SimpleITK ` +* `SimpleITK `__ Optional io plugin providing a wide variety of `formats `__. including specialized formats using in medical imaging. -* `imread ` +* `imread `__ Optional io plugin providing most standard `formats `__. diff --git a/README.md b/README.md index b7ae96d1..f1338716 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ - **Debian/Ubuntu:** ``sudo apt-get install python-skimage`` - **OSX:** ``pip install scikit-image`` -- **Anaconda:** ``conda install scikit-image +- **Anaconda:** ``conda install scikit-image`` - **Windows:** Download [Windows binaries](http://www.lfd.uci.edu/~gohlke/pythonlibs/#scikits.image) Also see diff --git a/RELEASE.txt b/RELEASE.txt index 66860662..915134ad 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -15,7 +15,7 @@ How to make a new release of ``skimage`` 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 +- Update the version number in ``skimage/__init__.py`` and ``bento.info`` and commit - Update the docs: @@ -31,7 +31,10 @@ How to make a new release of ``skimage`` - Add the version number as a tag in git:: - git tag v0.X.0 + git tag -s v0.X.0 + + (If you do not have a gpg key, use -m instead; it is important for + Debian packaging that the tags are annotated) - Push the new meta-data to github:: diff --git a/appveyor.yml b/appveyor.yml index df1ffd3e..0bd10075 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -13,17 +13,18 @@ environment: PYTHON_VERSION: "2.7" PYTHON_ARCH: "32" - - PYTHON: "C:\\Python27_64" - PYTHON_VERSION: "2.7" - PYTHON_ARCH: "64" + # disable other builds until they can be run in parallel + #- PYTHON: "C:\\Python27_64" + # PYTHON_VERSION: "2.7" + # PYTHON_ARCH: "64" - - PYTHON: "C:\\Python34_32" - PYTHON_VERSION: "3.4.2" - PYTHON_ARCH: "32" + #- PYTHON: "C:\\Python34_32" + # PYTHON_VERSION: "3.4.2" + # PYTHON_ARCH: "32" - - PYTHON: "C:\\Python24_64" - PYTHON_VERSION: "3.4.2" - PYTHON_ARCH: "64" + #- PYTHON: "C:\\Python24_64" + # PYTHON_VERSION: "3.4.2" + # PYTHON_ARCH: "64" install: # Install Python (from the official .msi of http://python.org) and pip when diff --git a/bento.info b/bento.info index 0af161d4..8587db3f 100644 --- a/bento.info +++ b/bento.info @@ -1,5 +1,5 @@ Name: scikit-image -Version: 0.11.0 +Version: 0.11.3 Summary: Image processing routines for SciPy Url: http://scikit-image.org DownloadUrl: http://github.com/scikit-image/scikit-image diff --git a/doc/README.md b/doc/README.md index 50deb4e2..c89500e7 100644 --- a/doc/README.md +++ b/doc/README.md @@ -2,13 +2,31 @@ To build docs, run `make` in this directory. `make help` lists all targets. ## Requirements ## -Sphinx is needed to build doc. Install with `pip install sphinx`. +Sphinx and Latex is needed to build doc. + +**Spinx:** +```sh +pip install sphinx +``` + +**Latex Ubuntu:** +```sh +sudo apt-get install -qq texlive texlive-latex-extra dvipng +``` + +**Latex Mac:** + +Install the full [MacTex](http://www.tug.org/mactex/) installation or install the smaller [BasicTex](http://www.tug.org/mactex/morepackages.html) and add *ucs* and *dvipng* packages: +```sh +sudo tlmgr install ucs dvipng +``` + ## Fixing Warnings ## - "citation not found: R###" $ cd doc/build; grep -rin R### . - There is probably an underscore after a reference + There is probably an underscore after a reference in the first line of a docstring (e.g. [1]_) - "Duplicate citation R###, other instance in..."" diff --git a/doc/ext/plot2rst.py b/doc/ext/plot2rst.py index 99fd9a3f..32811deb 100644 --- a/doc/ext/plot2rst.py +++ b/doc/ext/plot2rst.py @@ -186,6 +186,11 @@ def setup(app): def generate_example_galleries(app): cfg = app.builder.config + if isinstance(cfg.source_suffix, list): + cfg.source_suffix_str = cfg.source_suffix[0] + else: + cfg.source_suffix_str = cfg.source_suffix + doc_src = Path(os.path.abspath(app.builder.srcdir)) # path/to/doc/source if isinstance(cfg.plot2rst_paths, tuple): @@ -205,7 +210,7 @@ def generate_examples_and_gallery(example_dir, rst_dir, cfg): rst_dir.makedirs() # we create an index.rst with all examples - gallery_index = open(rst_dir.pjoin('index'+cfg.source_suffix), 'w') + gallery_index = open(rst_dir.pjoin('index'+cfg.source_suffix_str), 'w') # Here we don't use an os.walk, but we recurse only twice: flat is # better than nested. @@ -235,7 +240,7 @@ def write_gallery(gallery_index, src_dir, rst_dir, cfg, depth=0): cfg : config object Sphinx config object created by Sphinx. """ - index_name = cfg.plot2rst_index_name + cfg.source_suffix + index_name = cfg.plot2rst_index_name + cfg.source_suffix_str gallery_template = src_dir.pjoin(index_name) if not os.path.exists(gallery_template): print(src_dir) @@ -328,7 +333,8 @@ def write_example(src_name, src_dir, rst_dir, cfg): image_path = image_dir.pjoin(base_image_name + '_{0}.png') basename, py_ext = os.path.splitext(src_name) - rst_path = rst_dir.pjoin(basename + cfg.source_suffix) + + rst_path = rst_dir.pjoin(basename + cfg.source_suffix_str) notebook_path = notebook_dir.pjoin(basename + '.ipynb') if _plots_are_current(src_path, image_path) and rst_path.exists and \ diff --git a/doc/source/conf.py b/doc/source/conf.py index 94904305..794df662 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -68,10 +68,10 @@ copyright = '2013, the scikit-image team' # # The short X.Y version. -setup_lines = open('../../setup.py').readlines() +setup_lines = open('../../skimage/__init__.py').readlines() version = 'vUndefined' for l in setup_lines: - if l.startswith('VERSION'): + if l.startswith('__version__'): version = l.split("'")[1] break diff --git a/doc/tools/build_modref_templates.py b/doc/tools/build_modref_templates.py index e83ce980..2d959252 100644 --- a/doc/tools/build_modref_templates.py +++ b/doc/tools/build_modref_templates.py @@ -37,10 +37,10 @@ if __name__ == '__main__': installed_version = V(module.__version__) - setup_lines = open('../setup.py').readlines() + source_lines = open('../skimage/__init__.py').readlines() version = 'vUndefined' - for l in setup_lines: - if l.startswith('VERSION'): + for l in source_lines: + if l.startswith('__version__'): source_version = V(l.split("'")[1]) break diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 8ba529e5..9e5d1444 --- a/setup.py +++ b/setup.py @@ -17,28 +17,40 @@ 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.11.0' -PYTHON_VERSION = (2, 6) import os -import sys import setuptools from distutils.command.build_py import build_py -from distutils.version import LooseVersion -# These are manually checked. -# 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') -for line in data.splitlines(): - pkg, _, version_info = line.replace('==', '>=').partition('>=') - # 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) +with open('skimage/__init__.py') as fid: + for line in fid: + if line.startswith('__version__'): + VERSION = line.strip().split()[-1][1:-1] + break + +with open('requirements.txt') as fid: + INSTALL_REQUIRES = [l.strip() for l in fid.readlines() if l] + +# development versions do not have the cythonized files +if VERSION.endswith('dev'): + SETUP_REQUIRES = [r for r in INSTALL_REQUIRES if r.startswith('cython')] +else: + INSTALL_REQUIRES = [r for r in INSTALL_REQUIRES + if not r.startswith('cython')] + SETUP_REQUIRES = [] + + +# list requirements for PyPI +REQUIRES = [r.replace('>=', ' (>= ') + ')' + for r in INSTALL_REQUIRES + SETUP_REQUIRES] +REQUIRES = [r.replace('==', ' (== ') for r in REQUIRES] + + +# do not attempt to install numpy and scipy until they have eggs available +INSTALL_REQUIRES = [r for r in INSTALL_REQUIRES + if not r.startswith(('scipy', 'numpy'))] def configuration(parent_package='', top_path=None): @@ -59,72 +71,11 @@ def configuration(parent_package='', top_path=None): return config -def write_version_py(filename='skimage/version.py'): - template = """# THIS FILE IS GENERATED FROM THE SKIMAGE SETUP.PY -version='%s' -""" - - try: - vfile = open(os.path.join(os.path.dirname(__file__), - filename), 'w') - vfile.write(template % VERSION) - - except IOError: - raise IOError("Could not open/write to skimage/version.py - did you " - "install using sudo in the past? If so, run\n" - "sudo chown -R your_username ./*\n" - "from package root to fix permissions, and try again.") - - finally: - vfile.close() - - -def get_package_version(package): - for version_attr in ('__version__', 'VERSION', 'version'): - version_info = getattr(package, version_attr, None) - if version_info and str(version_attr) == version_attr: - 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(): - if package_name == 'cython': - package_name = 'Cython' - dep_error = '' - if package_name.lower() == 'pillow': - package_name = 'PIL.Image' - min_version = '1.1.7' - try: - package = __import__(package_name, - fromlist=[package_name.rpartition('.')[0]]) - except ImportError: - dep_error = ('You need `%s` version %s or later.' - % (package_name, min_version)) - else: - if package_name == 'PIL': - package_version = package.PILLOW_VERSION - else: - package_version = get_package_version(package) - - if LooseVersion(min_version) > LooseVersion(package_version): - dep_error = ('You need `%s` version %s or later,' - 'found version %s.' - % (package_name, min_version, - package_version)) - if dep_error: - raise ImportError(dep_error) - - if __name__ == "__main__": - - check_requirements() - - write_version_py() - + # purposely fail loudly if numpy or scipy are not available from numpy.distutils.core import setup + import scipy + setup( name=DISTNAME, description=DESCRIPTION, @@ -153,9 +104,9 @@ if __name__ == "__main__": ], configuration=configuration, - install_requires=[ - "six>=%s" % DEPENDENCIES['six'] - ], + setup_requires=SETUP_REQUIRES, + install_requires=INSTALL_REQUIRES, + requires=REQUIRES, packages=setuptools.find_packages(exclude=['doc']), include_package_data=True, zip_safe=False, # the package can run out of an .egg file diff --git a/skimage/__init__.py b/skimage/__init__.py index 8f386682..2ec70944 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -64,13 +64,7 @@ import warnings as _warnings 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" -else: - del version - +__version__ = '0.11.3' try: _imp.find_module('nose') diff --git a/skimage/_shared/testing.py b/skimage/_shared/testing.py index 1c41221b..f202fbff 100644 --- a/skimage/_shared/testing.py +++ b/skimage/_shared/testing.py @@ -161,7 +161,7 @@ def mono_check(plugin, fmt='png'): else: testing.assert_allclose(r3, img_as_uint(img)) - with expected_warnings(['precision loss']): + with expected_warnings(['precision loss|unclosed file']): img4 = img_as_int(img) if fmt.lower() in (('tif', 'tiff')): img4 -= 100 @@ -182,7 +182,7 @@ def setup_test(): """Default package level setup routine for skimage tests. Import packages known to raise errors, and then - force warnings to raise errors. + force warnings to raise errors. Set a random seed """ warnings.simplefilter('default') @@ -190,7 +190,7 @@ def setup_test(): from scipy.io import loadmat from skimage import viewer, filter np.random.seed(0) - warnings.simplefilter('error') + warnings.simplefilter('error') def teardown_test(): diff --git a/skimage/exposure/__init__.py b/skimage/exposure/__init__.py index d78cf927..1950f37a 100644 --- a/skimage/exposure/__init__.py +++ b/skimage/exposure/__init__.py @@ -1,6 +1,7 @@ from .exposure import histogram, equalize_hist, \ rescale_intensity, cumulative_distribution, \ - adjust_gamma, adjust_sigmoid, adjust_log + adjust_gamma, adjust_sigmoid, adjust_log, \ + is_low_contrast from ._adapthist import equalize_adapthist @@ -12,4 +13,5 @@ __all__ = ['histogram', 'cumulative_distribution', 'adjust_gamma', 'adjust_sigmoid', - 'adjust_log'] + 'adjust_log', + 'is_low_contrast'] diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index 8774cf9b..0cad7bbe 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -1,7 +1,8 @@ +from __future__ import division import warnings import numpy as np -from .. import img_as_float +from ..color import rgb2gray from ..util.dtype import dtype_range, dtype_limits @@ -463,3 +464,54 @@ def adjust_sigmoid(image, cutoff=0.5, gain=10, inv=False): out = (1 / (1 + np.exp(gain * (cutoff - image / scale)))) * scale return dtype(out) + + +def is_low_contrast(image, fraction_threshold=0.05, lower_percentile=1, + upper_percentile=99, method='linear'): + """Detemine if an image is low contrast. + + Parameters + ---------- + image : array-like + The image under test. + fraction_threshold : float, optional + The low contrast fraction threshold. An image is considered low- + contrast when its range of brightness spans less than this + fraction of its data type's full range. [1]_ + lower_bound : float, optional + Disregard values below this percentile when computing image contrast. + upper_bound : float, optional + Disregard values above this percentile when computing image contrast. + method : str, optional + The contrast determination method. Right now the only available + option is "linear". + + Returns + ------- + out : bool + True when the image is determined to be low contrast. + + References + ---------- + .. [1] http://scikit-image.org/docs/dev/user_guide/data_types.html + + Examples + -------- + >>> image = np.linspace(0, 0.04, 100) + >>> is_low_contrast(image) + True + >>> image[-1] = 1 + >>> is_low_contrast(image) + True + >>> is_low_contrast(image, upper_percentile=100) + False + """ + image = np.asanyarray(image) + if image.ndim == 3 and image.shape[2] in [3, 4]: + image = rgb2gray(image) + + dlimits = dtype_limits(image) + limits = np.percentile(image, [lower_percentile, upper_percentile]) + ratio = (limits[1] - limits[0]) / (dlimits[1] - dlimits[0]) + + return ratio < fraction_threshold diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py index 1316ea4e..265ce246 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -472,6 +472,22 @@ def test_negative(): assert_raises(ValueError, exposure.adjust_gamma, image) +def test_is_low_contrast(): + image = np.linspace(0, 0.04, 100) + assert exposure.is_low_contrast(image) + image[-1] = 1 + assert exposure.is_low_contrast(image) + assert not exposure.is_low_contrast(image, upper_percentile=100) + + image = (image * 255).astype(np.uint8) + assert exposure.is_low_contrast(image) + assert not exposure.is_low_contrast(image, upper_percentile=100) + + image = (image.astype(np.uint16)) * 2**8 + assert exposure.is_low_contrast(image) + assert not exposure.is_low_contrast(image, upper_percentile=100) + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() diff --git a/skimage/io/_io.py b/skimage/io/_io.py index f44bfd8b..e76c36f6 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -1,4 +1,5 @@ from io import BytesIO +import warnings import numpy as np import six @@ -6,6 +7,8 @@ import six from ..io.manage_plugins import call_plugin from ..color import rgb2grey from .util import file_or_url_context +from ..exposure import is_low_contrast +from .._shared._warnings import all_warnings __all__ = ['Image', 'imread', 'imread_collection', 'imsave', 'imshow', 'show'] @@ -152,6 +155,8 @@ def imsave(fname, arr, plugin=None, **plugin_args): Passed to the given plugin. """ + if is_low_contrast(arr): + warnings.warn('%s is a low contrast image' % fname) return call_plugin('imsave', fname, arr, plugin=plugin, **plugin_args) diff --git a/skimage/io/_plugins/gtk_plugin.py b/skimage/io/_plugins/gtk_plugin.py index 6130289c..327d762d 100644 --- a/skimage/io/_plugins/gtk_plugin.py +++ b/skimage/io/_plugins/gtk_plugin.py @@ -1,4 +1,4 @@ -from util import prepare_for_display, window_manager, GuiLockError +from .util import prepare_for_display, window_manager, GuiLockError try: # we try to aquire the gui lock first diff --git a/skimage/io/_plugins/matplotlib_plugin.py b/skimage/io/_plugins/matplotlib_plugin.py index 2de61ecf..32ebf2fd 100644 --- a/skimage/io/_plugins/matplotlib_plugin.py +++ b/skimage/io/_plugins/matplotlib_plugin.py @@ -2,7 +2,9 @@ from collections import namedtuple import numpy as np import warnings import matplotlib.pyplot as plt -from skimage.util import dtype as dtypes +from ...util import dtype as dtypes +from ...exposure import is_low_contrast +from ..._shared._warnings import all_warnings _default_colormap = 'gray' @@ -49,7 +51,7 @@ def _get_image_properties(image): out_of_range_float = (np.issubdtype(image.dtype, np.float) and (immin < lo or immax > hi)) low_dynamic_range = (immin != immax and - (float(immax - immin) / (hi - lo)) < (1. / 255)) + is_low_contrast(image)) unsupported_dtype = image.dtype not in dtypes._supported_types return ImageProperties(signed, out_of_range_float, @@ -72,8 +74,8 @@ def _raise_warnings(image_properties): warnings.warn("Low image dynamic range; displaying image with " "stretched contrast.") if ip.out_of_range_float: - warnings.warn("Float image out of standard range; displaying image " - "with stretched contrast.") + warnings.warn("Float image out of standard range; displaying " + "image with stretched contrast.") def _get_display_range(image): diff --git a/skimage/io/_plugins/q_color_mixer.py b/skimage/io/_plugins/q_color_mixer.py index 3fe9e29c..87402a37 100644 --- a/skimage/io/_plugins/q_color_mixer.py +++ b/skimage/io/_plugins/q_color_mixer.py @@ -2,7 +2,7 @@ from PyQt4 import QtGui, QtCore from PyQt4.QtGui import (QWidget, QStackedWidget, QSlider, QGridLayout, QLabel) -from util import ColorMixer +from .util import ColorMixer class IntelligentSlider(QWidget): diff --git a/skimage/io/_plugins/q_histogram.py b/skimage/io/_plugins/q_histogram.py index 0a60ce9f..4b1b545b 100644 --- a/skimage/io/_plugins/q_histogram.py +++ b/skimage/io/_plugins/q_histogram.py @@ -2,7 +2,7 @@ import numpy as np from PyQt4.QtGui import QWidget, QPainter, QGridLayout, QColor, QFrame -from util import histograms +from .util import histograms class ColorHistogram(QWidget): diff --git a/skimage/io/collection.py b/skimage/io/collection.py index 49d6aa25..8812740e 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -135,7 +135,8 @@ class ImageCollection(object): ic = ImageCollection('/tmp/*.png', load_func=imread_convert) For files with multiple images, the images will be flattened into a list - and added to the list of available images. + and added to the list of available images. In this case, ``load_func`` + should accept the keyword argument ``img_num``. Examples -------- @@ -165,7 +166,7 @@ class ImageCollection(object): self._numframes = self._find_images() else: self._files = load_pattern - self._numframes = len(load_pattern) + self._numframes = len(self._files) self._frame_index = None if conserve_memory: @@ -201,26 +202,22 @@ class ImageCollection(object): img = TiffFile(fname) index += [(fname, i) for i in range(len(img.pages))] else: - im = Image.open(fname) try: - # this will raise an IOError if the file is not readable - 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)) - else: - i = 0 - while True: - try: - im.seek(i) - except EOFError: - break - index.append((fname, i)) - i += 1 + im = Image.open(fname) + im.seek(0) + except (IOError, OSError): + index.append([fname, i]) + continue + i = 0 + while True: + try: + im.seek(i) + except EOFError: + break + index.append((fname, i)) + i += 1 if hasattr(im, 'fp') and im.fp: im.fp.close() - self._frame_index = index return len(index) @@ -254,13 +251,16 @@ class ImageCollection(object): if ((self.conserve_memory and n != self._cached) or (self.data[idx] is None)): + kwargs = self.load_func_kwargs if self._frame_index: fname, img_num = self._frame_index[n] - self.data[idx] = self.load_func(fname, img_num=img_num, - **self.load_func_kwargs) + if img_num > 0: + self.data[idx] = self.load_func(fname, img_num=img_num, + **kwargs) + else: + self.data[idx] = self.load_func(fname, **kwargs) else: - self.data[idx] = self.load_func(self.files[n], - **self.load_func_kwargs) + self.data[idx] = self.load_func(self.files[n], **kwargs) self._cached = n return self.data[idx] diff --git a/skimage/io/tests/test_freeimage.py b/skimage/io/tests/test_freeimage.py index 81f8058a..238229d1 100644 --- a/skimage/io/tests/test_freeimage.py +++ b/skimage/io/tests/test_freeimage.py @@ -1,6 +1,7 @@ import os import skimage as si import skimage.io as sio +from skimage import data_dir import numpy as np from numpy.testing import * @@ -112,5 +113,17 @@ def test_metadata(): assert meta[1][('EXIF_MAIN', 'Software')].startswith('I') +@skipif(not FI_available) +def test_collection(): + pattern = [os.path.join(data_dir, pic) + for pic in ['camera.png', 'color.png', 'multipage.tif']] + images = sio.ImageCollection(pattern[:-1]) + assert len(images) == 2 + assert len(images[:]) == 2 + + images = sio.ImageCollection(pattern) + assert len(images) == 3 + assert len(images[:]) == 3 + if __name__ == "__main__": run_module_suite() diff --git a/skimage/io/tests/test_mpl_imshow.py b/skimage/io/tests/test_mpl_imshow.py index aad51048..166c8cbc 100644 --- a/skimage/io/tests/test_mpl_imshow.py +++ b/skimage/io/tests/test_mpl_imshow.py @@ -87,7 +87,8 @@ def test_outside_standard_range(): def test_nonstandard_type(): plt.figure() - with expected_warnings(["Non-standard image type"]): + with expected_warnings(["Non-standard image type", + "Low image dynamic range"]): ax_im = io.imshow(im64) assert ax_im.get_clim() == (im64.min(), im64.max()) assert n_subplots(ax_im) == 2 diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index fc28ce89..e8909503 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -147,7 +147,8 @@ def test_imsave_filelike(): s = BytesIO() # save to file-like object - with expected_warnings(['precision loss|unclosed file']): + with expected_warnings(['precision loss|unclosed file', + 'is a low contrast image']): imsave(s, image) # read from file-like object diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index e2a4a51a..9731d6da 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -9,7 +9,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/fit.py b/skimage/measure/fit.py index 7de90291..f8e65117 100644 --- a/skimage/measure/fit.py +++ b/skimage/measure/fit.py @@ -4,9 +4,6 @@ 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) @@ -57,6 +54,11 @@ class LineModel(BaseModel): data : (N, 2) array N points with ``(x, y)`` coordinates, respectively. + Returns + ------- + success : bool + True, if model estimation succeeds. + """ _check_data_dim(data, dim=2) @@ -81,6 +83,8 @@ class LineModel(BaseModel): self.params = (dist, theta) + return True + def residuals(self, data): """Determine residuals of data to model. @@ -182,6 +186,11 @@ class CircleModel(BaseModel): data : (N, 2) array N points with ``(x, y)`` coordinates, respectively. + Returns + ------- + success : bool + True, if model estimation succeeds. + """ _check_data_dim(data, dim=2) @@ -217,6 +226,8 @@ class CircleModel(BaseModel): self.params = params + return True + def residuals(self, data): """Determine residuals of data to model. @@ -313,6 +324,11 @@ class EllipseModel(BaseModel): data : (N, 2) array N points with ``(x, y)`` coordinates, respectively. + Returns + ------- + success : bool + True, if model estimation succeeds. + """ _check_data_dim(data, dim=2) @@ -373,6 +389,8 @@ class EllipseModel(BaseModel): self.params = params[:5] + return True + def residuals(self, data): """Determine residuals of data to model. @@ -471,7 +489,6 @@ class EllipseModel(BaseModel): 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 @@ -482,21 +499,31 @@ def _dynamic_max_trials(n_inliers, n_samples, min_samples, probability): 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. - """ + if n_inliers == 0: + return np.inf + + nom = 1 - probability + if nom == 0: + return np.inf + inlier_ratio = n_inliers / float(n_samples) - nom = max(_EPSILON, 1 - probability) - denom = max(_EPSILON, 1 - inlier_ratio ** min_samples) - if nom == 1: + denom = 1 - inlier_ratio ** min_samples + if denom == 0: + return 1 + elif denom == 1: + return np.inf + + nom = np.log(nom) + denom = np.log(denom) + if denom == 0: return 0 - if denom == 1: - return float('inf') - return abs(float(np.ceil(np.log(nom) / np.log(denom)))) + + return int(np.ceil(nom / denom)) def ransac(data, model_class, min_samples, residual_threshold, @@ -542,9 +569,11 @@ def ransac(data, model_class, min_samples, residual_threshold, model_class : object Object with the following object methods: - * ``estimate(*data)`` + * ``success = estimate(*data)`` * ``residuals(*data)`` + where `success` indicates whether the model estimation succeeded + (`True` or `None` for success, `False` for failure). min_samples : int The minimum number of data points to fit a model to. residual_threshold : float @@ -612,6 +641,7 @@ def ransac(data, model_class, min_samples, residual_threshold, >>> model = EllipseModel() >>> model.estimate(data) + True >>> model.params # doctest: +SKIP array([ -3.30354146e+03, -2.87791160e+03, 5.59062118e+03, 7.84365066e+00, 7.19203152e-01]) @@ -688,7 +718,12 @@ def ransac(data, model_class, min_samples, residual_threshold, # estimate model for current random sample set sample_model = model_class() - sample_model.estimate(*samples) + + success = sample_model.estimate(*samples) + + if success is not None: # backwards compatibility + if not success: + continue # check if estimated model is valid if is_model_valid is not None and not is_model_valid(sample_model, diff --git a/skimage/measure/tests/test_fit.py b/skimage/measure/tests/test_fit.py index 1b960d4b..7f98c971 100644 --- a/skimage/measure/tests/test_fit.py +++ b/skimage/measure/tests/test_fit.py @@ -233,9 +233,9 @@ def test_ransac_dynamic_max_trials(): # 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')) + # e = 0%, min_samples = 5 + assert_equal(_dynamic_max_trials(1, 100, 5, 0), 0) + assert_equal(_dynamic_max_trials(1, 100, 5, 1), np.inf) def test_ransac_invalid_input(): diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index 3ac735c7..487d782e 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -26,7 +26,7 @@ INTENSITY_SAMPLE[1, 9:11] = 2 def test_all_props(): region = regionprops(SAMPLE, INTENSITY_SAMPLE)[0] for prop in PROPS: - assert_equal(region[prop], getattr(region, PROPS[prop])) + assert_almost_equal(region[prop], getattr(region, PROPS[prop])) def test_dtype(): diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index c94e3b76..33448353 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -263,6 +263,11 @@ class ProjectiveTransform(GeometricTransform): dst : (N, 2) array Destination coordinates. + Returns + ------- + success : bool + True, if model estimation succeeds. + """ try: @@ -270,7 +275,7 @@ class ProjectiveTransform(GeometricTransform): dst_matrix, dst = _center_and_normalize_points(dst) except ZeroDivisionError: self.params = np.nan * np.empty((3, 3)) - return + return False xs = src[:, 0] ys = src[:, 1] @@ -309,6 +314,8 @@ class ProjectiveTransform(GeometricTransform): self.params = H + return True + def __add__(self, other): """Combine this transformation with another. @@ -459,6 +466,11 @@ class PiecewiseAffineTransform(GeometricTransform): dst : (N, 2) array Destination coordinates. + Returns + ------- + success : bool + True, if model estimation succeeds. + """ # forward piecewise affine @@ -481,6 +493,8 @@ class PiecewiseAffineTransform(GeometricTransform): affine.estimate(dst[tri, :], src[tri, :]) self.inverse_affines.append(affine) + return True + def __call__(self, coords): """Apply forward transformation. @@ -658,6 +672,11 @@ class SimilarityTransform(ProjectiveTransform): dst : (N, 2) array Destination coordinates. + Returns + ------- + success : bool + True, if model estimation succeeds. + """ try: @@ -665,7 +684,7 @@ class SimilarityTransform(ProjectiveTransform): dst_matrix, dst = _center_and_normalize_points(dst) except ZeroDivisionError: self.params = np.nan * np.empty((3, 3)) - return + return False xs = src[:, 0] ys = src[:, 1] @@ -699,6 +718,7 @@ class SimilarityTransform(ProjectiveTransform): self.params = S + return True @property def scale(self): @@ -798,6 +818,11 @@ class PolynomialTransform(GeometricTransform): order : int, optional Polynomial order (number of coefficients is order + 1). + Returns + ------- + success : bool + True, if model estimation succeeds. + """ xs = src[:, 0] ys = src[:, 1] @@ -828,6 +853,8 @@ class PolynomialTransform(GeometricTransform): self.params = params.reshape((2, u // 2)) + return True + def __call__(self, coords): """Apply forward transformation. diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index 0e7bc060..1c594534 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -11,6 +11,10 @@ dtype_range = {np.bool_: (False, True), np.uint16: (0, 65535), np.int8: (-128, 127), np.int16: (-32768, 32767), + np.int64: (-2**63, 2**63 - 1), + np.uint64: (0, 2**64 - 1), + np.int32: (-2**31, 2**31 - 1), + np.uint32: (0, 2**32 - 1), np.float32: (-1, 1), np.float64: (-1, 1)} diff --git a/skimage/viewer/tests/test_widgets.py b/skimage/viewer/tests/test_widgets.py index 66b86fa4..8d0e05e0 100644 --- a/skimage/viewer/tests/test_widgets.py +++ b/skimage/viewer/tests/test_widgets.py @@ -101,6 +101,11 @@ def test_save_buttons(): timer = QtCore.QTimer() timer.singleShot(100, QtGui.QApplication.quit) + # exercise the button clicks + sv.save_stack.click() + sv.save_file.click() + + # call the save functions directly sv.save_to_stack() with expected_warnings(['precision loss']): sv.save_to_file(filename) diff --git a/skimage/viewer/widgets/__init__.py b/skimage/viewer/widgets/__init__.py index efa9fe9d..2705f371 100644 --- a/skimage/viewer/widgets/__init__.py +++ b/skimage/viewer/widgets/__init__.py @@ -1,2 +1,20 @@ +""" +Widgets for interacting with ImageViewer. + +These widgets should be added to a Plugin subclass using its `add_widget` +method or calling:: + + plugin += Widget(...) + +on a Plugin instance. The Plugin will delegate action based on the widget's +parameter type specified by its `ptype` attribute, which can be:: + + 'arg' : positional argument passed to Plugin's `filter_image` method. + 'kwarg' : keyword argument passed to Plugin's `filter_image` method. + 'plugin' : attribute of Plugin. You'll probably need to add a class + property of the same name that updates the display. + +""" + from .core import * from .history import * diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index ae8be4e7..71f0bc50 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -1,20 +1,3 @@ -""" -Widgets for interacting with ImageViewer. - -These widgets should be added to a Plugin subclass using its `add_widget` -method or calling:: - - plugin += Widget(...) - -on a Plugin instance. The Plugin will delegate action based on the widget's -parameter type specified by its `ptype` attribute, which can be: - - 'arg' : positional argument passed to Plugin's `filter_image` method. - 'kwarg' : keyword argument passed to Plugin's `filter_image` method. - 'plugin' : attribute of Plugin. You'll probably need to add a class - property of the same name that updates the display. - -""" from ..qt import QtWidgets, QtCore, Qt from ..utils import RequiredAttr @@ -275,7 +258,7 @@ class CheckBox(BaseWidget): added to a plugin. """ - def __init__(self, name, value=False, alignment='center', ptype='kwarg', + def __init__(self, name, value=False, alignment='center', ptype='kwarg', callback=None): super(CheckBox, self).__init__(name, ptype, callback) diff --git a/skimage/viewer/widgets/history.py b/skimage/viewer/widgets/history.py index 3b7f58b0..6229e525 100644 --- a/skimage/viewer/widgets/history.py +++ b/skimage/viewer/widgets/history.py @@ -80,9 +80,9 @@ class SaveButtons(BaseWidget): notify(msg) def save_to_file(self, filename=None): - if filename is None: + if not filename: filename = dialogs.save_file_dialog() - if filename is None: + if not filename: return image = self.plugin.filtered_image if image.dtype == np.bool: