update
This commit is contained in:
+184
@@ -0,0 +1,184 @@
|
||||
"""Image Processing for Python
|
||||
|
||||
``scikit-image`` (a.k.a. ``skimage``) is a collection of algorithms for image
|
||||
processing and computer vision.
|
||||
|
||||
The main package of ``skimage`` only provides a few utilities for converting
|
||||
between image data types; for most features, you need to import one of the
|
||||
following subpackages:
|
||||
|
||||
Subpackages
|
||||
-----------
|
||||
color
|
||||
Color space conversion.
|
||||
data
|
||||
Test images and example data.
|
||||
draw
|
||||
Drawing primitives (lines, text, etc.) that operate on NumPy arrays.
|
||||
exposure
|
||||
Image intensity adjustment, e.g., histogram equalization, etc.
|
||||
feature
|
||||
Feature detection and extraction, e.g., texture analysis corners, etc.
|
||||
filters
|
||||
Sharpening, edge finding, rank filters, thresholding, etc.
|
||||
graph
|
||||
Graph-theoretic operations, e.g., shortest paths.
|
||||
io
|
||||
Reading, saving, and displaying images and video.
|
||||
measure
|
||||
Measurement of image properties, e.g., region properties and contours.
|
||||
metrics
|
||||
Metrics corresponding to images, e.g. distance metrics, similarity, etc.
|
||||
morphology
|
||||
Morphological operations, e.g., opening or skeletonization.
|
||||
restoration
|
||||
Restoration algorithms, e.g., deconvolution algorithms, denoising, etc.
|
||||
segmentation
|
||||
Partitioning an image into multiple regions.
|
||||
transform
|
||||
Geometric and other transforms, e.g., rotation or the Radon transform.
|
||||
util
|
||||
Generic utilities.
|
||||
|
||||
Utility Functions
|
||||
-----------------
|
||||
img_as_float
|
||||
Convert an image to floating point format, with values in [0, 1].
|
||||
Is similar to `img_as_float64`, but will not convert lower-precision
|
||||
floating point arrays to `float64`.
|
||||
img_as_float32
|
||||
Convert an image to single-precision (32-bit) floating point format,
|
||||
with values in [0, 1].
|
||||
img_as_float64
|
||||
Convert an image to double-precision (64-bit) floating point format,
|
||||
with values in [0, 1].
|
||||
img_as_uint
|
||||
Convert an image to unsigned integer format, with values in [0, 65535].
|
||||
img_as_int
|
||||
Convert an image to signed integer format, with values in [-32768, 32767].
|
||||
img_as_ubyte
|
||||
Convert an image to unsigned byte format, with values in [0, 255].
|
||||
img_as_bool
|
||||
Convert an image to boolean format, with values either True or False.
|
||||
dtype_limits
|
||||
Return intensity limits, i.e. (min, max) tuple, of the image's dtype.
|
||||
|
||||
"""
|
||||
|
||||
__version__ = '0.24.0'
|
||||
|
||||
import lazy_loader as lazy
|
||||
|
||||
__getattr__, __lazy_dir__, _ = lazy.attach_stub(__name__, __file__)
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __lazy_dir__() + ['__version__']
|
||||
|
||||
|
||||
# Logic for checking for improper install and importing while in the source
|
||||
# tree when package has not been installed inplace.
|
||||
# Code adapted from scikit-learn's __check_build module.
|
||||
_INPLACE_MSG = """
|
||||
It appears that you are importing a local scikit-image source tree. For
|
||||
this, you need to have an inplace install. Maybe you are in the source
|
||||
directory and you need to try from another location."""
|
||||
|
||||
_STANDARD_MSG = """
|
||||
Your install of scikit-image appears to be broken.
|
||||
Try re-installing the package following the instructions at:
|
||||
https://scikit-image.org/docs/stable/user_guide/install.html"""
|
||||
|
||||
|
||||
def _raise_build_error(e):
|
||||
# Raise a comprehensible error
|
||||
import os.path as osp
|
||||
|
||||
local_dir = osp.split(__file__)[0]
|
||||
msg = _STANDARD_MSG
|
||||
if local_dir == "skimage":
|
||||
# Picking up the local install: this will work only if the
|
||||
# install is an 'inplace build'
|
||||
msg = _INPLACE_MSG
|
||||
raise ImportError(
|
||||
f"{e}\nIt seems that scikit-image has not been built correctly.\n{msg}"
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
# This variable is injected in the __builtins__ by the build
|
||||
# process. It used to enable importing subpackages of skimage when
|
||||
# the binaries are not built
|
||||
__SKIMAGE_SETUP__
|
||||
except NameError:
|
||||
__SKIMAGE_SETUP__ = False
|
||||
|
||||
if __SKIMAGE_SETUP__:
|
||||
import sys
|
||||
|
||||
sys.stderr.write('Partial import of skimage during the build process.\n')
|
||||
# We are not importing the rest of the scikit during the build
|
||||
# process, as it may not be compiled yet
|
||||
else:
|
||||
try:
|
||||
from ._shared import geometry
|
||||
|
||||
del geometry
|
||||
except ImportError as e:
|
||||
_raise_build_error(e)
|
||||
|
||||
# Legacy imports into the root namespace; not advertised in __all__
|
||||
from .util.dtype import (
|
||||
dtype_limits,
|
||||
img_as_float32,
|
||||
img_as_float64,
|
||||
img_as_float,
|
||||
img_as_int,
|
||||
img_as_uint,
|
||||
img_as_ubyte,
|
||||
img_as_bool,
|
||||
)
|
||||
|
||||
from .util.lookfor import lookfor
|
||||
|
||||
from .data import data_dir
|
||||
|
||||
|
||||
if 'dev' in __version__:
|
||||
# Append last commit date and hash to dev version information, if available
|
||||
|
||||
import subprocess
|
||||
import os.path
|
||||
|
||||
try:
|
||||
p = subprocess.Popen(
|
||||
['git', 'log', '-1', '--format="%h %aI"'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
cwd=os.path.dirname(__file__),
|
||||
)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError:
|
||||
pass # If skimage is built with emscripten which does not support processes
|
||||
else:
|
||||
out, err = p.communicate()
|
||||
if p.returncode == 0:
|
||||
git_hash, git_date = (
|
||||
out.decode('utf-8')
|
||||
.strip()
|
||||
.replace('"', '')
|
||||
.split('T')[0]
|
||||
.replace('-', '')
|
||||
.split()
|
||||
)
|
||||
|
||||
__version__ = '+'.join(
|
||||
[tag for tag in __version__.split('+') if not tag.startswith('git')]
|
||||
)
|
||||
__version__ += f'+git{git_date}.{git_hash}'
|
||||
|
||||
from skimage._shared.tester import PytestTester # noqa
|
||||
|
||||
test = PytestTester(__name__)
|
||||
del PytestTester
|
||||
@@ -0,0 +1,41 @@
|
||||
submodules = [
|
||||
'color',
|
||||
'data',
|
||||
'draw',
|
||||
'exposure',
|
||||
'feature',
|
||||
'filters',
|
||||
'future',
|
||||
'graph',
|
||||
'io',
|
||||
'measure',
|
||||
'metrics',
|
||||
'morphology',
|
||||
'registration',
|
||||
'restoration',
|
||||
'segmentation',
|
||||
'transform',
|
||||
'util',
|
||||
]
|
||||
|
||||
__all__ = submodules + ['__version__'] # noqa: F822
|
||||
|
||||
from . import (
|
||||
color,
|
||||
data,
|
||||
draw,
|
||||
exposure,
|
||||
feature,
|
||||
filters,
|
||||
future,
|
||||
graph,
|
||||
io,
|
||||
measure,
|
||||
metrics,
|
||||
morphology,
|
||||
registration,
|
||||
restoration,
|
||||
segmentation,
|
||||
transform,
|
||||
util,
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
from .version_requirements import is_installed
|
||||
import sys
|
||||
import platform
|
||||
|
||||
has_mpl = is_installed("matplotlib", ">=3.3")
|
||||
|
||||
is_wasm = (sys.platform == "emscripten") or (platform.machine() in ["wasm32", "wasm64"])
|
||||
@@ -0,0 +1,54 @@
|
||||
__all__ = ['polygon_clip', 'polygon_area']
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .version_requirements import require
|
||||
|
||||
|
||||
@require("matplotlib", ">=3.3")
|
||||
def polygon_clip(rp, cp, r0, c0, r1, c1):
|
||||
"""Clip a polygon to the given bounding box.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
rp, cp : (K,) ndarray of double
|
||||
Row and column coordinates of the polygon.
|
||||
(r0, c0), (r1, c1) : double
|
||||
Top-left and bottom-right coordinates of the bounding box.
|
||||
|
||||
Returns
|
||||
-------
|
||||
r_clipped, c_clipped : (L,) ndarray of double
|
||||
Coordinates of clipped polygon.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This makes use of Sutherland-Hodgman clipping as implemented in
|
||||
AGG 2.4 and exposed in Matplotlib.
|
||||
|
||||
"""
|
||||
from matplotlib import path, transforms
|
||||
|
||||
poly = path.Path(np.vstack((rp, cp)).T, closed=True)
|
||||
clip_rect = transforms.Bbox([[r0, c0], [r1, c1]])
|
||||
poly_clipped = poly.clip_to_bbox(clip_rect).to_polygons()[0]
|
||||
|
||||
return poly_clipped[:, 0], poly_clipped[:, 1]
|
||||
|
||||
|
||||
def polygon_area(pr, pc):
|
||||
"""Compute the area of a polygon.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pr, pc : (K,) array of float
|
||||
Polygon row and column coordinates.
|
||||
|
||||
Returns
|
||||
-------
|
||||
a : float
|
||||
Area of the polygon.
|
||||
"""
|
||||
pr = np.asarray(pr)
|
||||
pc = np.asarray(pc)
|
||||
return 0.5 * np.abs(np.sum((pc[:-1] * pr[1:]) - (pc[1:] * pr[:-1])))
|
||||
@@ -0,0 +1,28 @@
|
||||
from tempfile import NamedTemporaryFile
|
||||
from contextlib import contextmanager
|
||||
import os
|
||||
|
||||
|
||||
@contextmanager
|
||||
def temporary_file(suffix=''):
|
||||
"""Yield a writeable temporary filename that is deleted on context exit.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
suffix : string, optional
|
||||
The suffix for the file.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from skimage import io
|
||||
>>> with temporary_file('.tif') as tempfile:
|
||||
... im = np.arange(25, dtype=np.uint8).reshape((5, 5))
|
||||
... io.imsave(tempfile, im)
|
||||
... assert np.all(io.imread(tempfile) == im)
|
||||
"""
|
||||
with NamedTemporaryFile(suffix=suffix, delete=False) as tempfile_stream:
|
||||
tempfile = tempfile_stream.name
|
||||
|
||||
yield tempfile
|
||||
os.remove(tempfile)
|
||||
@@ -0,0 +1,149 @@
|
||||
from contextlib import contextmanager
|
||||
import sys
|
||||
import warnings
|
||||
import re
|
||||
import functools
|
||||
import os
|
||||
|
||||
__all__ = ['all_warnings', 'expected_warnings', 'warn']
|
||||
|
||||
|
||||
# A version of `warnings.warn` with a default stacklevel of 2.
|
||||
# functool is used so as not to increase the call stack accidentally
|
||||
warn = functools.partial(warnings.warn, stacklevel=2)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def all_warnings():
|
||||
"""
|
||||
Context for use in testing to ensure that all warnings are raised.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import warnings
|
||||
>>> def foo():
|
||||
... warnings.warn(RuntimeWarning("bar"), stacklevel=2)
|
||||
|
||||
We raise the warning once, while the warning filter is set to "once".
|
||||
Hereafter, the warning is invisible, even with custom filters:
|
||||
|
||||
>>> with warnings.catch_warnings():
|
||||
... warnings.simplefilter('once')
|
||||
... foo() # doctest: +SKIP
|
||||
|
||||
We can now run ``foo()`` without a warning being raised:
|
||||
|
||||
>>> from numpy.testing import assert_warns
|
||||
>>> foo() # doctest: +SKIP
|
||||
|
||||
To catch the warning, we call in the help of ``all_warnings``:
|
||||
|
||||
>>> with all_warnings():
|
||||
... assert_warns(RuntimeWarning, foo)
|
||||
"""
|
||||
# _warnings.py is on the critical import path.
|
||||
# Since this is a testing only function, we lazy import inspect.
|
||||
import inspect
|
||||
|
||||
# Whenever a warning is triggered, Python adds a __warningregistry__
|
||||
# member to the *calling* module. The exercise here is to find
|
||||
# and eradicate all those breadcrumbs that were left lying around.
|
||||
#
|
||||
# We proceed by first searching all parent calling frames and explicitly
|
||||
# clearing their warning registries (necessary for the doctests above to
|
||||
# pass). Then, we search for all submodules of skimage and clear theirs
|
||||
# as well (necessary for the skimage test suite to pass).
|
||||
|
||||
frame = inspect.currentframe()
|
||||
if frame:
|
||||
for f in inspect.getouterframes(frame):
|
||||
f[0].f_locals['__warningregistry__'] = {}
|
||||
del frame
|
||||
|
||||
for mod_name, mod in list(sys.modules.items()):
|
||||
try:
|
||||
mod.__warningregistry__.clear()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
yield w
|
||||
|
||||
|
||||
@contextmanager
|
||||
def expected_warnings(matching):
|
||||
r"""Context for use in testing to catch known warnings matching regexes
|
||||
|
||||
Parameters
|
||||
----------
|
||||
matching : None or a list of strings or compiled regexes
|
||||
Regexes for the desired warning to catch
|
||||
If matching is None, this behaves as a no-op.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> rng = np.random.default_rng()
|
||||
>>> image = rng.integers(0, 2**16, size=(100, 100), dtype=np.uint16)
|
||||
>>> # rank filters are slow when bit-depth exceeds 10 bits
|
||||
>>> from skimage import filters
|
||||
>>> with expected_warnings(['Bad rank filter performance']):
|
||||
... median_filtered = filters.rank.median(image)
|
||||
|
||||
Notes
|
||||
-----
|
||||
Uses `all_warnings` to ensure all warnings are raised.
|
||||
Upon exiting, it checks the recorded warnings for the desired matching
|
||||
pattern(s).
|
||||
Raises a ValueError if any match was not found or an unexpected
|
||||
warning was raised.
|
||||
Allows for three types of behaviors: `and`, `or`, and `optional` matches.
|
||||
This is done to accommodate different build environments or loop conditions
|
||||
that may produce different warnings. The behaviors can be combined.
|
||||
If you pass multiple patterns, you get an orderless `and`, where all of the
|
||||
warnings must be raised.
|
||||
If you use the `|` operator in a pattern, you can catch one of several
|
||||
warnings.
|
||||
Finally, you can use `|\A\Z` in a pattern to signify it as optional.
|
||||
|
||||
"""
|
||||
if isinstance(matching, str):
|
||||
raise ValueError(
|
||||
'``matching`` should be a list of strings and not ' 'a string itself.'
|
||||
)
|
||||
|
||||
# Special case for disabling the context manager
|
||||
if matching is None:
|
||||
yield None
|
||||
return
|
||||
|
||||
strict_warnings = os.environ.get('SKIMAGE_TEST_STRICT_WARNINGS', '1')
|
||||
if strict_warnings.lower() == 'true':
|
||||
strict_warnings = True
|
||||
elif strict_warnings.lower() == 'false':
|
||||
strict_warnings = False
|
||||
else:
|
||||
strict_warnings = bool(int(strict_warnings))
|
||||
|
||||
with all_warnings() as w:
|
||||
# enter context
|
||||
yield w
|
||||
# exited user context, check the recorded warnings
|
||||
# Allow users to provide None
|
||||
while None in matching:
|
||||
matching.remove(None)
|
||||
remaining = [m for m in matching if r'\A\Z' not in m.split('|')]
|
||||
for warn in w:
|
||||
found = False
|
||||
for match in matching:
|
||||
if re.search(match, str(warn.message)) is not None:
|
||||
found = True
|
||||
if match in remaining:
|
||||
remaining.remove(match)
|
||||
if strict_warnings and not found:
|
||||
raise ValueError(f'Unexpected warning: {str(warn.message)}')
|
||||
if strict_warnings and (len(remaining) > 0):
|
||||
newline = "\n"
|
||||
msg = f"No warning raised matching:{newline}{newline.join(remaining)}"
|
||||
raise ValueError(msg)
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Compatibility helpers for dependencies."""
|
||||
|
||||
from packaging.version import parse
|
||||
|
||||
import numpy as np
|
||||
import scipy as sp
|
||||
|
||||
|
||||
__all__ = [
|
||||
"NP_COPY_IF_NEEDED",
|
||||
"SCIPY_CG_TOL_PARAM_NAME",
|
||||
]
|
||||
|
||||
|
||||
NUMPY_LT_2_0_0 = parse(np.__version__) < parse('2.0.0.dev0')
|
||||
|
||||
# With NumPy 2.0.0, `copy=False` now raises a ValueError if the copy cannot be
|
||||
# made. The previous behavior to only copy if needed is provided with `copy=None`.
|
||||
# During the transition period, use this symbol instead.
|
||||
# Remove once NumPy 2.0.0 is the minimal required version.
|
||||
# https://numpy.org/devdocs/release/2.0.0-notes.html#new-copy-keyword-meaning-for-array-and-asarray-constructors
|
||||
# https://github.com/numpy/numpy/pull/25168
|
||||
NP_COPY_IF_NEEDED = False if NUMPY_LT_2_0_0 else None
|
||||
|
||||
|
||||
SCIPY_LT_1_12 = parse(sp.__version__) < parse('1.12')
|
||||
|
||||
# Starting in SciPy v1.12, 'scipy.sparse.linalg.cg' keyword argument `tol` is
|
||||
# deprecated in favor of `rtol`.
|
||||
SCIPY_CG_TOL_PARAM_NAME = "tol" if SCIPY_LT_1_12 else "rtol"
|
||||
@@ -0,0 +1,125 @@
|
||||
import numpy as np
|
||||
from scipy.spatial import cKDTree, distance
|
||||
|
||||
|
||||
def _ensure_spacing(coord, spacing, p_norm, max_out):
|
||||
"""Returns a subset of coord where a minimum spacing is guaranteed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
coord : ndarray
|
||||
The coordinates of the considered points.
|
||||
spacing : float
|
||||
the maximum allowed spacing between the points.
|
||||
p_norm : float
|
||||
Which Minkowski p-norm to use. Should be in the range [1, inf].
|
||||
A finite large p may cause a ValueError if overflow can occur.
|
||||
``inf`` corresponds to the Chebyshev distance and 2 to the
|
||||
Euclidean distance.
|
||||
max_out: int
|
||||
If not None, at most the first ``max_out`` candidates are
|
||||
returned.
|
||||
|
||||
Returns
|
||||
-------
|
||||
output : ndarray
|
||||
A subset of coord where a minimum spacing is guaranteed.
|
||||
|
||||
"""
|
||||
|
||||
# Use KDtree to find the peaks that are too close to each other
|
||||
tree = cKDTree(coord)
|
||||
|
||||
indices = tree.query_ball_point(coord, r=spacing, p=p_norm)
|
||||
rejected_peaks_indices = set()
|
||||
naccepted = 0
|
||||
for idx, candidates in enumerate(indices):
|
||||
if idx not in rejected_peaks_indices:
|
||||
# keep current point and the points at exactly spacing from it
|
||||
candidates.remove(idx)
|
||||
dist = distance.cdist(
|
||||
[coord[idx]], coord[candidates], distance.minkowski, p=p_norm
|
||||
).reshape(-1)
|
||||
candidates = [c for c, d in zip(candidates, dist) if d < spacing]
|
||||
|
||||
# candidates.remove(keep)
|
||||
rejected_peaks_indices.update(candidates)
|
||||
naccepted += 1
|
||||
if max_out is not None and naccepted >= max_out:
|
||||
break
|
||||
|
||||
# Remove the peaks that are too close to each other
|
||||
output = np.delete(coord, tuple(rejected_peaks_indices), axis=0)
|
||||
if max_out is not None:
|
||||
output = output[:max_out]
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def ensure_spacing(
|
||||
coords,
|
||||
spacing=1,
|
||||
p_norm=np.inf,
|
||||
min_split_size=50,
|
||||
max_out=None,
|
||||
*,
|
||||
max_split_size=2000,
|
||||
):
|
||||
"""Returns a subset of coord where a minimum spacing is guaranteed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
coords : array_like
|
||||
The coordinates of the considered points.
|
||||
spacing : float
|
||||
the maximum allowed spacing between the points.
|
||||
p_norm : float
|
||||
Which Minkowski p-norm to use. Should be in the range [1, inf].
|
||||
A finite large p may cause a ValueError if overflow can occur.
|
||||
``inf`` corresponds to the Chebyshev distance and 2 to the
|
||||
Euclidean distance.
|
||||
min_split_size : int
|
||||
Minimum split size used to process ``coords`` by batch to save
|
||||
memory. If None, the memory saving strategy is not applied.
|
||||
max_out : int
|
||||
If not None, only the first ``max_out`` candidates are returned.
|
||||
max_split_size : int
|
||||
Maximum split size used to process ``coords`` by batch to save
|
||||
memory. This number was decided by profiling with a large number
|
||||
of points. Too small a number results in too much looping in
|
||||
Python instead of C, slowing down the process, while too large
|
||||
a number results in large memory allocations, slowdowns, and,
|
||||
potentially, in the process being killed -- see gh-6010. See
|
||||
benchmark results `here
|
||||
<https://github.com/scikit-image/scikit-image/pull/6035#discussion_r751518691>`_.
|
||||
|
||||
Returns
|
||||
-------
|
||||
output : array_like
|
||||
A subset of coord where a minimum spacing is guaranteed.
|
||||
|
||||
"""
|
||||
|
||||
output = coords
|
||||
if len(coords):
|
||||
coords = np.atleast_2d(coords)
|
||||
if min_split_size is None:
|
||||
batch_list = [coords]
|
||||
else:
|
||||
coord_count = len(coords)
|
||||
split_idx = [min_split_size]
|
||||
split_size = min_split_size
|
||||
while coord_count - split_idx[-1] > max_split_size:
|
||||
split_size *= 2
|
||||
split_idx.append(split_idx[-1] + min(split_size, max_split_size))
|
||||
batch_list = np.array_split(coords, split_idx)
|
||||
|
||||
output = np.zeros((0, coords.shape[1]), dtype=coords.dtype)
|
||||
for batch in batch_list:
|
||||
output = _ensure_spacing(
|
||||
np.vstack([output, batch]), spacing, p_norm, max_out
|
||||
)
|
||||
if max_out is not None and len(output) >= max_out:
|
||||
break
|
||||
|
||||
return output
|
||||
@@ -0,0 +1,73 @@
|
||||
import numpy as np
|
||||
|
||||
# Define classes of supported dtypes and Python scalar types
|
||||
# Variables ending in `_dtypes` only contain numpy.dtypes of the respective
|
||||
# class; variables ending in `_types` additionally include Python scalar types.
|
||||
signed_integer_dtypes = {np.int8, np.int16, np.int32, np.int64}
|
||||
signed_integer_types = signed_integer_dtypes | {int}
|
||||
|
||||
unsigned_integer_dtypes = {np.uint8, np.uint16, np.uint32, np.uint64}
|
||||
|
||||
integer_dtypes = signed_integer_dtypes | unsigned_integer_dtypes
|
||||
integer_types = signed_integer_types | unsigned_integer_dtypes
|
||||
|
||||
floating_dtypes = {np.float16, np.float32, np.float64}
|
||||
floating_types = floating_dtypes | {float}
|
||||
|
||||
complex_dtypes = {np.complex64, np.complex128}
|
||||
complex_types = complex_dtypes | {complex}
|
||||
|
||||
inexact_dtypes = floating_dtypes | complex_dtypes
|
||||
inexact_types = floating_types | complex_types
|
||||
|
||||
bool_types = {np.dtype(bool), bool}
|
||||
|
||||
numeric_dtypes = integer_dtypes | inexact_dtypes | {np.bool_}
|
||||
numeric_types = integer_types | inexact_types | bool_types
|
||||
|
||||
|
||||
def numeric_dtype_min_max(dtype):
|
||||
"""Return minimum and maximum representable value for a given dtype.
|
||||
|
||||
A convenient wrapper around `numpy.finfo` and `numpy.iinfo` that
|
||||
additionally supports numpy.bool as well.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dtype : numpy.dtype
|
||||
The dtype. Tries to convert Python "types" such as int or float, to
|
||||
the corresponding NumPy dtype.
|
||||
|
||||
Returns
|
||||
-------
|
||||
min, max : number
|
||||
Minimum and maximum of the given `dtype`. These scalars are themselves
|
||||
of the given `dtype`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> numeric_dtype_min_max(np.uint8)
|
||||
(0, 255)
|
||||
>>> numeric_dtype_min_max(bool)
|
||||
(False, True)
|
||||
>>> numeric_dtype_min_max(np.float64)
|
||||
(-1.7976931348623157e+308, 1.7976931348623157e+308)
|
||||
>>> numeric_dtype_min_max(int)
|
||||
(-9223372036854775808, 9223372036854775807)
|
||||
"""
|
||||
dtype = np.dtype(dtype)
|
||||
if np.issubdtype(dtype, np.integer):
|
||||
info = np.iinfo(dtype)
|
||||
min_ = dtype.type(info.min)
|
||||
max_ = dtype.type(info.max)
|
||||
elif np.issubdtype(dtype, np.inexact):
|
||||
info = np.finfo(dtype)
|
||||
min_ = info.min
|
||||
max_ = info.max
|
||||
elif np.issubdtype(dtype, np.dtype(bool)):
|
||||
min_ = dtype.type(False)
|
||||
max_ = dtype.type(True)
|
||||
else:
|
||||
raise ValueError(f"unsupported dtype {dtype!r}")
|
||||
return min_, max_
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,47 @@
|
||||
/* A fast approximation of the exponential function.
|
||||
* Reference [1]: https://schraudolph.org/pubs/Schraudolph99.pdf
|
||||
* Reference [2]: https://doi.org/10.1162/089976600300015033
|
||||
* Additional improvements by Leonid Bloch. */
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* use just EXP_A = 1512775 for integer version, to avoid FP calculations */
|
||||
#define EXP_A (1512775.3951951856938) /* 2^20/ln2 */
|
||||
/* For min. RMS error */
|
||||
#define EXP_BC 1072632447 /* 1023*2^20 - 60801 */
|
||||
/* For min. max. relative error */
|
||||
/* #define EXP_BC 1072647449 */ /* 1023*2^20 - 45799 */
|
||||
/* For min. mean relative error */
|
||||
/* #define EXP_BC 1072625005 */ /* 1023*2^20 - 68243 */
|
||||
|
||||
__inline double _fast_exp (double y)
|
||||
{
|
||||
union
|
||||
{
|
||||
double d;
|
||||
struct { int32_t i, j; } n;
|
||||
char t[8];
|
||||
} _eco;
|
||||
|
||||
_eco.n.i = 1;
|
||||
|
||||
switch(_eco.t[0]) {
|
||||
case 1:
|
||||
/* Little endian */
|
||||
_eco.n.j = (int32_t)(EXP_A*(y)) + EXP_BC;
|
||||
_eco.n.i = 0;
|
||||
break;
|
||||
case 0:
|
||||
/* Big endian */
|
||||
_eco.n.i = (int32_t)(EXP_A*(y)) + EXP_BC;
|
||||
_eco.n.j = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
return _eco.d;
|
||||
}
|
||||
|
||||
__inline float _fast_expf (float y)
|
||||
{
|
||||
return (float)_fast_exp((double)y);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Filters used across multiple skimage submodules.
|
||||
|
||||
These are defined here to avoid circular imports.
|
||||
|
||||
The unit tests remain under skimage/filters/tests/
|
||||
"""
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
import numpy as np
|
||||
from scipy import ndimage as ndi
|
||||
|
||||
from .._shared.utils import (
|
||||
_supported_float_type,
|
||||
convert_to_float,
|
||||
deprecate_parameter,
|
||||
DEPRECATED,
|
||||
)
|
||||
|
||||
|
||||
@deprecate_parameter(
|
||||
"output", new_name="out", start_version="0.23", stop_version="0.25"
|
||||
)
|
||||
def gaussian(
|
||||
image,
|
||||
sigma=1,
|
||||
output=DEPRECATED,
|
||||
mode='nearest',
|
||||
cval=0,
|
||||
preserve_range=False,
|
||||
truncate=4.0,
|
||||
*,
|
||||
channel_axis=None,
|
||||
out=None,
|
||||
):
|
||||
"""Multi-dimensional Gaussian filter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image : ndarray
|
||||
Input image (grayscale or color) to filter.
|
||||
sigma : scalar or sequence of scalars, optional
|
||||
Standard deviation for Gaussian kernel. The standard
|
||||
deviations of the Gaussian filter are given for each axis as a
|
||||
sequence, or as a single number, in which case it is equal for
|
||||
all axes.
|
||||
mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional
|
||||
The ``mode`` parameter determines how the array borders are
|
||||
handled, where ``cval`` is the value when mode is equal to
|
||||
'constant'. Default is 'nearest'.
|
||||
cval : scalar, optional
|
||||
Value to fill past edges of input if ``mode`` is 'constant'. Default
|
||||
is 0.0
|
||||
preserve_range : bool, optional
|
||||
If True, keep the original range of values. Otherwise, the input
|
||||
``image`` is converted according to the conventions of ``img_as_float``
|
||||
(Normalized first to values [-1.0 ; 1.0] or [0 ; 1.0] depending on
|
||||
dtype of input)
|
||||
|
||||
For more information, see:
|
||||
https://scikit-image.org/docs/dev/user_guide/data_types.html
|
||||
truncate : float, optional
|
||||
Truncate the filter at this many standard deviations.
|
||||
channel_axis : int or None, optional
|
||||
If None, the image is assumed to be a grayscale (single channel) image.
|
||||
Otherwise, this parameter indicates which axis of the array corresponds
|
||||
to channels.
|
||||
|
||||
.. versionadded:: 0.19
|
||||
`channel_axis` was added in 0.19.
|
||||
out : ndarray, optional
|
||||
If given, the filtered image will be stored in this array.
|
||||
|
||||
.. versionadded:: 0.23
|
||||
`out` was added in 0.23.
|
||||
|
||||
Returns
|
||||
-------
|
||||
filtered_image : ndarray
|
||||
the filtered array
|
||||
|
||||
Notes
|
||||
-----
|
||||
This function is a wrapper around :func:`scipy.ndimage.gaussian_filter`.
|
||||
|
||||
Integer arrays are converted to float.
|
||||
|
||||
`out` should be of floating-point data type since `gaussian` converts the
|
||||
input `image` to float. If `out` is not provided, another array
|
||||
will be allocated and returned as the result.
|
||||
|
||||
The multi-dimensional filter is implemented as a sequence of
|
||||
one-dimensional convolution filters. The intermediate arrays are
|
||||
stored in the same data type as the output. Therefore, for output
|
||||
types with a limited precision, the results may be imprecise
|
||||
because intermediate results may be stored with insufficient
|
||||
precision.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import skimage as ski
|
||||
>>> a = np.zeros((3, 3))
|
||||
>>> a[1, 1] = 1
|
||||
>>> a
|
||||
array([[0., 0., 0.],
|
||||
[0., 1., 0.],
|
||||
[0., 0., 0.]])
|
||||
>>> ski.filters.gaussian(a, sigma=0.4) # mild smoothing
|
||||
array([[0.00163116, 0.03712502, 0.00163116],
|
||||
[0.03712502, 0.84496158, 0.03712502],
|
||||
[0.00163116, 0.03712502, 0.00163116]])
|
||||
>>> ski.filters.gaussian(a, sigma=1) # more smoothing
|
||||
array([[0.05855018, 0.09653293, 0.05855018],
|
||||
[0.09653293, 0.15915589, 0.09653293],
|
||||
[0.05855018, 0.09653293, 0.05855018]])
|
||||
>>> # Several modes are possible for handling boundaries
|
||||
>>> ski.filters.gaussian(a, sigma=1, mode='reflect')
|
||||
array([[0.08767308, 0.12075024, 0.08767308],
|
||||
[0.12075024, 0.16630671, 0.12075024],
|
||||
[0.08767308, 0.12075024, 0.08767308]])
|
||||
>>> # For RGB images, each is filtered separately
|
||||
>>> image = ski.data.astronaut()
|
||||
>>> filtered_img = ski.filters.gaussian(image, sigma=1, channel_axis=-1)
|
||||
|
||||
"""
|
||||
if np.any(np.asarray(sigma) < 0.0):
|
||||
raise ValueError("Sigma values less than zero are not valid")
|
||||
if channel_axis is not None:
|
||||
# do not filter across channels
|
||||
if not isinstance(sigma, Iterable):
|
||||
sigma = [sigma] * (image.ndim - 1)
|
||||
if len(sigma) == image.ndim - 1:
|
||||
sigma = list(sigma)
|
||||
sigma.insert(channel_axis % image.ndim, 0)
|
||||
image = convert_to_float(image, preserve_range)
|
||||
float_dtype = _supported_float_type(image.dtype)
|
||||
image = image.astype(float_dtype, copy=False)
|
||||
if (out is not None) and (not np.issubdtype(out.dtype, np.floating)):
|
||||
raise ValueError(f"dtype of `out` must be float; got {out.dtype!r}.")
|
||||
return ndi.gaussian_filter(
|
||||
image, sigma, output=out, mode=mode, cval=cval, truncate=truncate
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,131 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def _show_skimage_info():
|
||||
import skimage
|
||||
|
||||
print(f"skimage version {skimage.__version__}")
|
||||
|
||||
|
||||
class PytestTester:
|
||||
"""
|
||||
Pytest test runner.
|
||||
|
||||
This class is made available in ``skimage._shared.testing``, and a test
|
||||
function is typically added to a package's __init__.py like so::
|
||||
|
||||
from skimage._shared.testing import PytestTester
|
||||
test = PytestTester(__name__)
|
||||
del PytestTester
|
||||
|
||||
Calling this test function finds and runs all tests associated with the
|
||||
module and all its sub-modules.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
module_name : str
|
||||
Full path to the package to test.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
module_name : module name
|
||||
The name of the module to test.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, module_name):
|
||||
self.module_name = module_name
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
label='fast',
|
||||
verbose=1,
|
||||
extra_argv=None,
|
||||
doctests=False,
|
||||
coverage=False,
|
||||
durations=-1,
|
||||
tests=None,
|
||||
):
|
||||
"""
|
||||
Run tests for module using pytest.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : {'fast', 'full'}, optional
|
||||
Identifies the tests to run. When set to 'fast', tests decorated
|
||||
with `pytest.mark.slow` are skipped, when 'full', the slow marker
|
||||
is ignored.
|
||||
verbose : int, optional
|
||||
Verbosity value for test outputs, in the range 1-3. Default is 1.
|
||||
extra_argv : list, optional
|
||||
List with any extra arguments to pass to pytests.
|
||||
doctests : bool, optional
|
||||
.. note:: Not supported
|
||||
coverage : bool, optional
|
||||
If True, report coverage of scikit-image code. Default is False.
|
||||
Requires installation of (pip) pytest-cov.
|
||||
durations : int, optional
|
||||
If < 0, do nothing, If 0, report time of all tests, if > 0,
|
||||
report the time of the slowest `timer` tests. Default is -1.
|
||||
tests : test or list of tests
|
||||
Tests to be executed with pytest '--pyargs'
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : bool
|
||||
Return True on success, false otherwise.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
module = sys.modules[self.module_name]
|
||||
module_path = os.path.abspath(module.__path__[0])
|
||||
|
||||
# setup the pytest arguments
|
||||
pytest_args = ["-l"]
|
||||
|
||||
# offset verbosity. The "-q" cancels a "-v".
|
||||
pytest_args += ["-q"]
|
||||
|
||||
# Filter out annoying import messages. Want these in both develop and
|
||||
# release mode.
|
||||
pytest_args += [
|
||||
"-W ignore:Not importing directory",
|
||||
"-W ignore:numpy.dtype size changed",
|
||||
"-W ignore:numpy.ufunc size changed",
|
||||
]
|
||||
|
||||
if doctests:
|
||||
raise ValueError("Doctests not supported")
|
||||
|
||||
if extra_argv:
|
||||
pytest_args += list(extra_argv)
|
||||
|
||||
if verbose > 1:
|
||||
pytest_args += ["-" + "v" * (verbose - 1)]
|
||||
|
||||
if coverage:
|
||||
pytest_args += ["--cov=" + module_path]
|
||||
|
||||
if label == "fast":
|
||||
pytest_args += ["-m", "not slow"]
|
||||
elif label != "full":
|
||||
pytest_args += ["-m", label]
|
||||
|
||||
if durations >= 0:
|
||||
pytest_args += [f"--durations={durations}"]
|
||||
|
||||
if tests is None:
|
||||
tests = [self.module_name]
|
||||
|
||||
pytest_args += ["--pyargs"] + list(tests)
|
||||
|
||||
# run tests.
|
||||
_show_skimage_info()
|
||||
|
||||
try:
|
||||
code = pytest.main(pytest_args)
|
||||
except SystemExit as exc:
|
||||
code = exc.code
|
||||
|
||||
return code == 0
|
||||
@@ -0,0 +1,303 @@
|
||||
"""
|
||||
Testing utilities.
|
||||
"""
|
||||
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import functools
|
||||
import inspect
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
import numpy as np
|
||||
from numpy import testing
|
||||
from numpy.testing import (
|
||||
TestCase,
|
||||
assert_,
|
||||
assert_warns,
|
||||
assert_no_warnings,
|
||||
assert_equal,
|
||||
assert_almost_equal,
|
||||
assert_array_equal,
|
||||
assert_allclose,
|
||||
assert_array_almost_equal,
|
||||
assert_array_almost_equal_nulp,
|
||||
assert_array_less,
|
||||
)
|
||||
|
||||
from .. import data, io
|
||||
from ..data._fetchers import _fetch
|
||||
from ..util import img_as_uint, img_as_float, img_as_int, img_as_ubyte
|
||||
from ._warnings import expected_warnings
|
||||
from ._dependency_checks import is_wasm
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
skipif = pytest.mark.skipif
|
||||
xfail = pytest.mark.xfail
|
||||
parametrize = pytest.mark.parametrize
|
||||
raises = pytest.raises
|
||||
fixture = pytest.fixture
|
||||
|
||||
SKIP_RE = re.compile(r"(\s*>>>.*?)(\s*)#\s*skip\s+if\s+(.*)$")
|
||||
|
||||
# true if python is running in 32bit mode
|
||||
# Calculate the size of a void * pointer in bits
|
||||
# https://docs.python.org/3/library/struct.html
|
||||
arch32 = struct.calcsize("P") * 8 == 32
|
||||
|
||||
|
||||
def assert_less(a, b, msg=None):
|
||||
message = f"{a!r} is not lower than {b!r}"
|
||||
if msg is not None:
|
||||
message += ": " + msg
|
||||
assert a < b, message
|
||||
|
||||
|
||||
def assert_greater(a, b, msg=None):
|
||||
message = f"{a!r} is not greater than {b!r}"
|
||||
if msg is not None:
|
||||
message += ": " + msg
|
||||
assert a > b, message
|
||||
|
||||
|
||||
def doctest_skip_parser(func):
|
||||
"""Decorator replaces custom skip test markup in doctests
|
||||
|
||||
Say a function has a docstring::
|
||||
|
||||
>>> something, HAVE_AMODULE, HAVE_BMODULE = 0, False, False
|
||||
>>> something # skip if not HAVE_AMODULE
|
||||
0
|
||||
>>> something # skip if HAVE_BMODULE
|
||||
0
|
||||
|
||||
This decorator will evaluate the expression after ``skip if``. If this
|
||||
evaluates to True, then the comment is replaced by ``# doctest: +SKIP``. If
|
||||
False, then the comment is just removed. The expression is evaluated in the
|
||||
``globals`` scope of `func`.
|
||||
|
||||
For example, if the module global ``HAVE_AMODULE`` is False, and module
|
||||
global ``HAVE_BMODULE`` is False, the returned function will have docstring::
|
||||
|
||||
>>> something # doctest: +SKIP
|
||||
>>> something + else # doctest: +SKIP
|
||||
>>> something # doctest: +SKIP
|
||||
|
||||
"""
|
||||
lines = func.__doc__.split('\n')
|
||||
new_lines = []
|
||||
for line in lines:
|
||||
match = SKIP_RE.match(line)
|
||||
if match is None:
|
||||
new_lines.append(line)
|
||||
continue
|
||||
code, space, expr = match.groups()
|
||||
|
||||
try:
|
||||
# Works as a function decorator
|
||||
if eval(expr, func.__globals__):
|
||||
code = code + space + "# doctest: +SKIP"
|
||||
except AttributeError:
|
||||
# Works as a class decorator
|
||||
if eval(expr, func.__init__.__globals__):
|
||||
code = code + space + "# doctest: +SKIP"
|
||||
|
||||
new_lines.append(code)
|
||||
func.__doc__ = "\n".join(new_lines)
|
||||
return func
|
||||
|
||||
|
||||
def roundtrip(image, plugin, suffix):
|
||||
"""Save and read an image using a specified plugin"""
|
||||
if '.' not in suffix:
|
||||
suffix = '.' + suffix
|
||||
with NamedTemporaryFile(suffix=suffix, delete=False) as temp_file:
|
||||
fname = temp_file.name
|
||||
io.imsave(fname, image, plugin=plugin)
|
||||
new = io.imread(fname, plugin=plugin)
|
||||
try:
|
||||
os.remove(fname)
|
||||
except Exception:
|
||||
pass
|
||||
return new
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
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, r2.astype(bool))
|
||||
|
||||
img3 = img_as_float(img)
|
||||
r3 = roundtrip(img3, plugin, fmt)
|
||||
testing.assert_allclose(r3, img)
|
||||
|
||||
img4 = img_as_int(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)
|
||||
testing.assert_allclose(r5, img)
|
||||
|
||||
|
||||
def mono_check(plugin, fmt='png'):
|
||||
"""Check the roundtrip behavior for images that support most types.
|
||||
|
||||
All major input types should be handled.
|
||||
"""
|
||||
|
||||
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, r2.astype(bool))
|
||||
|
||||
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)
|
||||
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)
|
||||
testing.assert_allclose(r5, img5)
|
||||
|
||||
|
||||
def fetch(data_filename):
|
||||
"""Attempt to fetch data, but if unavailable, skip the tests."""
|
||||
try:
|
||||
return _fetch(data_filename)
|
||||
except (ConnectionError, ModuleNotFoundError):
|
||||
pytest.skip(f'Unable to download {data_filename}', allow_module_level=True)
|
||||
|
||||
|
||||
# Ref: about the lack of threading support in WASM, please see
|
||||
# https://github.com/pyodide/pyodide/issues/237
|
||||
def run_in_parallel(num_threads=2, warnings_matching=None):
|
||||
"""Decorator to run the same function multiple times in parallel.
|
||||
|
||||
This decorator is useful to ensure that separate threads execute
|
||||
concurrently and correctly while releasing the GIL.
|
||||
|
||||
It is currently skipped when running on WASM-based platforms, as
|
||||
the threading module is not supported.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
num_threads : int, optional
|
||||
The number of times the function is run in parallel.
|
||||
|
||||
warnings_matching: list or None
|
||||
This parameter is passed on to `expected_warnings` so as not to have
|
||||
race conditions with the warnings filters. A single
|
||||
`expected_warnings` context manager is used for all threads.
|
||||
If None, then no warnings are checked.
|
||||
|
||||
"""
|
||||
|
||||
assert num_threads > 0
|
||||
|
||||
def wrapper(func):
|
||||
if is_wasm:
|
||||
# Threading isn't supported on WASM, return early
|
||||
return func
|
||||
|
||||
import threading
|
||||
|
||||
@functools.wraps(func)
|
||||
def inner(*args, **kwargs):
|
||||
with expected_warnings(warnings_matching):
|
||||
threads = []
|
||||
for i in range(num_threads - 1):
|
||||
thread = threading.Thread(target=func, args=args, kwargs=kwargs)
|
||||
threads.append(thread)
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
|
||||
func(*args, **kwargs)
|
||||
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
return inner
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def assert_stacklevel(warnings, *, offset=-1):
|
||||
"""Assert correct stacklevel of captured warnings.
|
||||
|
||||
When scikit-image raises warnings, the stacklevel should ideally be set
|
||||
so that the origin of the warnings will point to the public function
|
||||
that was called by the user and not necessarily the very place where the
|
||||
warnings were emitted (which may be inside of some internal function).
|
||||
This utility function helps with checking that
|
||||
the stacklevel was set correctly on warnings captured by `pytest.warns`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
warnings : collections.abc.Iterable[warning.WarningMessage]
|
||||
Warnings that were captured by `pytest.warns`.
|
||||
offset : int, optional
|
||||
Offset from the line this function is called to the line were the
|
||||
warning is supposed to originate from. For multiline calls, the
|
||||
first line is relevant. Defaults to -1 which corresponds to the line
|
||||
right above the one where this function is called.
|
||||
|
||||
Raises
|
||||
------
|
||||
AssertionError
|
||||
If a warning in `warnings` does not match the expected line number or
|
||||
file name.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> def test_something():
|
||||
... with pytest.warns(UserWarning, match="some message") as record:
|
||||
... something_raising_a_warning()
|
||||
... assert_stacklevel(record)
|
||||
...
|
||||
>>> def test_another_thing():
|
||||
... with pytest.warns(UserWarning, match="some message") as record:
|
||||
... iam_raising_many_warnings(
|
||||
... "A long argument that forces the call to wrap."
|
||||
... )
|
||||
... assert_stacklevel(record, offset=-3)
|
||||
"""
|
||||
frame = inspect.stack()[1].frame # 0 is current frame, 1 is outer frame
|
||||
line_number = frame.f_lineno + offset
|
||||
filename = frame.f_code.co_filename
|
||||
expected = f"{filename}:{line_number}"
|
||||
for warning in warnings:
|
||||
actual = f"{warning.filename}:{warning.lineno}"
|
||||
assert actual == expected, f"{actual} != {expected}"
|
||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
.CondaPkg/env/Lib/site-packages/skimage/_shared/tests/__pycache__/test_interpolation.cpython-312.pyc
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,91 @@
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from scipy.spatial.distance import pdist, minkowski
|
||||
|
||||
from skimage._shared.coord import ensure_spacing
|
||||
|
||||
|
||||
@pytest.mark.parametrize("p", [1, 2, np.inf])
|
||||
@pytest.mark.parametrize("size", [30, 50, None])
|
||||
def test_ensure_spacing_trivial(p, size):
|
||||
# --- Empty input
|
||||
assert ensure_spacing([], p_norm=p) == []
|
||||
|
||||
# --- A unique point
|
||||
coord = np.random.randn(1, 2)
|
||||
assert np.array_equal(coord, ensure_spacing(coord, p_norm=p, min_split_size=size))
|
||||
|
||||
# --- Verified spacing
|
||||
coord = np.random.randn(100, 2)
|
||||
|
||||
# --- 0 spacing
|
||||
assert np.array_equal(
|
||||
coord, ensure_spacing(coord, spacing=0, p_norm=p, min_split_size=size)
|
||||
)
|
||||
|
||||
# Spacing is chosen to be half the minimum distance
|
||||
spacing = pdist(coord, metric=minkowski, p=p).min() * 0.5
|
||||
|
||||
out = ensure_spacing(coord, spacing=spacing, p_norm=p, min_split_size=size)
|
||||
|
||||
assert np.array_equal(coord, out)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ndim", [1, 2, 3, 4, 5])
|
||||
@pytest.mark.parametrize("size", [2, 10, None])
|
||||
def test_ensure_spacing_nD(ndim, size):
|
||||
coord = np.ones((5, ndim))
|
||||
|
||||
expected = np.ones((1, ndim))
|
||||
|
||||
assert np.array_equal(ensure_spacing(coord, min_split_size=size), expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("p", [1, 2, np.inf])
|
||||
@pytest.mark.parametrize("size", [50, 100, None])
|
||||
def test_ensure_spacing_batch_processing(p, size):
|
||||
coord = np.random.randn(100, 2)
|
||||
|
||||
# --- Consider the average distance btween the point as spacing
|
||||
spacing = np.median(pdist(coord, metric=minkowski, p=p))
|
||||
|
||||
expected = ensure_spacing(coord, spacing=spacing, p_norm=p)
|
||||
|
||||
assert np.array_equal(
|
||||
ensure_spacing(coord, spacing=spacing, p_norm=p, min_split_size=size), expected
|
||||
)
|
||||
|
||||
|
||||
def test_max_batch_size():
|
||||
"""Small batches are slow, large batches -> large allocations -> also slow.
|
||||
|
||||
https://github.com/scikit-image/scikit-image/pull/6035#discussion_r751518691
|
||||
"""
|
||||
coords = np.random.randint(low=0, high=1848, size=(40000, 2))
|
||||
tstart = time.time()
|
||||
ensure_spacing(coords, spacing=100, min_split_size=50, max_split_size=2000)
|
||||
dur1 = time.time() - tstart
|
||||
|
||||
tstart = time.time()
|
||||
ensure_spacing(coords, spacing=100, min_split_size=50, max_split_size=20000)
|
||||
dur2 = time.time() - tstart
|
||||
|
||||
# Originally checked dur1 < dur2 to assert that the default batch size was
|
||||
# faster than a much larger batch size. However, on rare occasion a CI test
|
||||
# case would fail with dur1 ~5% larger than dur2. To be more robust to
|
||||
# variable load or differences across architectures, we relax this here.
|
||||
assert dur1 < 1.33 * dur2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("p", [1, 2, np.inf])
|
||||
@pytest.mark.parametrize("size", [30, 50, None])
|
||||
def test_ensure_spacing_p_norm(p, size):
|
||||
coord = np.random.randn(100, 2)
|
||||
|
||||
# --- Consider the average distance btween the point as spacing
|
||||
spacing = np.median(pdist(coord, metric=minkowski, p=p))
|
||||
out = ensure_spacing(coord, spacing=spacing, p_norm=p, min_split_size=size)
|
||||
|
||||
assert pdist(out, metric=minkowski, p=p).min() > spacing
|
||||
@@ -0,0 +1,14 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
from ..dtype import numeric_dtype_min_max, numeric_types
|
||||
|
||||
|
||||
class Test_numeric_dtype_min_max:
|
||||
@pytest.mark.parametrize("dtype", numeric_types)
|
||||
def test_all_numeric_types(self, dtype):
|
||||
min_, max_ = numeric_dtype_min_max(dtype)
|
||||
assert np.isscalar(min_)
|
||||
assert np.isscalar(max_)
|
||||
assert min_ < max_
|
||||
@@ -0,0 +1,20 @@
|
||||
from ..fast_exp import fast_exp
|
||||
import numpy as np
|
||||
|
||||
|
||||
def test_fast_exp():
|
||||
X = np.linspace(-5, 0, 5000, endpoint=True)
|
||||
|
||||
# Ground truth
|
||||
Y = np.exp(X)
|
||||
|
||||
# Approximation at double precision
|
||||
_y_f64 = np.array([fast_exp['float64_t'](x) for x in X])
|
||||
|
||||
# Approximation at single precision
|
||||
_y_f32 = np.array(
|
||||
[fast_exp['float32_t'](x) for x in X.astype('float32')], dtype='float32'
|
||||
)
|
||||
|
||||
for _y in [_y_f64, _y_f32]:
|
||||
assert np.abs(Y - _y).mean() < 3e-3
|
||||
@@ -0,0 +1,81 @@
|
||||
import pytest
|
||||
from skimage._shared._geometry import polygon_clip, polygon_area
|
||||
|
||||
import numpy as np
|
||||
from numpy.testing import assert_equal, assert_almost_equal
|
||||
|
||||
pytest.importorskip("matplotlib")
|
||||
|
||||
|
||||
hand = np.array(
|
||||
[
|
||||
[1.64516129, 1.16145833],
|
||||
[1.64516129, 1.59375],
|
||||
[1.35080645, 1.921875],
|
||||
[1.375, 2.18229167],
|
||||
[1.68548387, 1.9375],
|
||||
[1.60887097, 2.55208333],
|
||||
[1.68548387, 2.69791667],
|
||||
[1.76209677, 2.56770833],
|
||||
[1.83064516, 1.97395833],
|
||||
[1.89516129, 2.75],
|
||||
[1.9516129, 2.84895833],
|
||||
[2.01209677, 2.76041667],
|
||||
[1.99193548, 1.99479167],
|
||||
[2.11290323, 2.63020833],
|
||||
[2.2016129, 2.734375],
|
||||
[2.25403226, 2.60416667],
|
||||
[2.14919355, 1.953125],
|
||||
[2.30645161, 2.36979167],
|
||||
[2.39112903, 2.36979167],
|
||||
[2.41532258, 2.1875],
|
||||
[2.1733871, 1.703125],
|
||||
[2.07782258, 1.16666667],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_polygon_area():
|
||||
x = [0, 0, 1, 1]
|
||||
y = [0, 1, 1, 0]
|
||||
|
||||
assert_almost_equal(polygon_area(y, x), 1)
|
||||
|
||||
x = [0, 0, 1]
|
||||
y = [0, 1, 1]
|
||||
|
||||
assert_almost_equal(polygon_area(y, x), 0.5)
|
||||
|
||||
x = [0, 0, 0.5, 1, 1, 0.5]
|
||||
y = [0, 1, 0.5, 1, 0, 0.5]
|
||||
|
||||
assert_almost_equal(polygon_area(y, x), 0.5)
|
||||
|
||||
|
||||
def test_poly_clip():
|
||||
x = [0, 1, 2, 1]
|
||||
y = [0, -1, 0, 1]
|
||||
|
||||
yc, xc = polygon_clip(y, x, 0, 0, 1, 1)
|
||||
assert_equal(polygon_area(yc, xc), 0.5)
|
||||
|
||||
x = [-1, 1.5, 1.5, -1]
|
||||
y = [0.5, 0.5, 1.5, 1.5]
|
||||
yc, xc = polygon_clip(y, x, 0, 0, 1, 1)
|
||||
assert_equal(polygon_area(yc, xc), 0.5)
|
||||
|
||||
|
||||
def test_hand_clip():
|
||||
(r0, c0, r1, c1) = (1.0, 1.5, 2.1, 2.5)
|
||||
clip_r, clip_c = polygon_clip(hand[:, 1], hand[:, 0], r0, c0, r1, c1)
|
||||
assert_equal(clip_r.size, 19)
|
||||
assert_equal(clip_r[0], clip_r[-1])
|
||||
assert_equal(clip_c[0], clip_c[-1])
|
||||
|
||||
(r0, c0, r1, c1) = (1.0, 1.5, 1.7, 2.5)
|
||||
clip_r, clip_c = polygon_clip(hand[:, 1], hand[:, 0], r0, c0, r1, c1)
|
||||
assert_equal(clip_r.size, 6)
|
||||
|
||||
(r0, c0, r1, c1) = (1.0, 1.5, 1.5, 2.5)
|
||||
clip_r, clip_c = polygon_clip(hand[:, 1], hand[:, 0], r0, c0, r1, c1)
|
||||
assert_equal(clip_r.size, 5)
|
||||
@@ -0,0 +1,28 @@
|
||||
from skimage._shared.interpolation import coord_map_py
|
||||
from skimage._shared.testing import assert_array_equal
|
||||
|
||||
|
||||
def test_coord_map():
|
||||
symmetric = [coord_map_py(4, n, 'S') for n in range(-6, 6)]
|
||||
expected_symmetric = [2, 3, 3, 2, 1, 0, 0, 1, 2, 3, 3, 2]
|
||||
assert_array_equal(symmetric, expected_symmetric)
|
||||
|
||||
wrap = [coord_map_py(4, n, 'W') for n in range(-6, 6)]
|
||||
expected_wrap = [2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1]
|
||||
assert_array_equal(wrap, expected_wrap)
|
||||
|
||||
edge = [coord_map_py(4, n, 'E') for n in range(-6, 6)]
|
||||
expected_edge = [0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 3]
|
||||
assert_array_equal(edge, expected_edge)
|
||||
|
||||
reflect = [coord_map_py(4, n, 'R') for n in range(-6, 6)]
|
||||
expected_reflect = [0, 1, 2, 3, 2, 1, 0, 1, 2, 3, 2, 1]
|
||||
assert_array_equal(reflect, expected_reflect)
|
||||
|
||||
reflect = [coord_map_py(1, n, 'R') for n in range(-6, 6)]
|
||||
expected_reflect = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
assert_array_equal(reflect, expected_reflect)
|
||||
|
||||
other = [coord_map_py(4, n, 'undefined') for n in range(-6, 6)]
|
||||
expected_other = list(range(-6, 6))
|
||||
assert_array_equal(other, expected_other)
|
||||
@@ -0,0 +1,41 @@
|
||||
import numpy as np
|
||||
from skimage._shared.utils import safe_as_int
|
||||
from skimage._shared import testing
|
||||
|
||||
|
||||
def test_int_cast_not_possible():
|
||||
with testing.raises(ValueError):
|
||||
safe_as_int(7.1)
|
||||
with testing.raises(ValueError):
|
||||
safe_as_int([7.1, 0.9])
|
||||
with testing.raises(ValueError):
|
||||
safe_as_int(np.r_[7.1, 0.9])
|
||||
with testing.raises(ValueError):
|
||||
safe_as_int((7.1, 0.9))
|
||||
with testing.raises(ValueError):
|
||||
safe_as_int(((3, 4, 1), (2, 7.6, 289)))
|
||||
with testing.raises(ValueError):
|
||||
safe_as_int(7.1, 0.09)
|
||||
with testing.raises(ValueError):
|
||||
safe_as_int([7.1, 0.9], 0.09)
|
||||
with testing.raises(ValueError):
|
||||
safe_as_int(np.r_[7.1, 0.9], 0.09)
|
||||
with testing.raises(ValueError):
|
||||
safe_as_int((7.1, 0.9), 0.09)
|
||||
with testing.raises(ValueError):
|
||||
safe_as_int(((3, 4, 1), (2, 7.6, 289)), 0.25)
|
||||
|
||||
|
||||
def test_int_cast_possible():
|
||||
testing.assert_equal(safe_as_int(7.1, atol=0.11), 7)
|
||||
testing.assert_equal(safe_as_int(-7.1, atol=0.11), -7)
|
||||
testing.assert_equal(safe_as_int(41.9, atol=0.11), 42)
|
||||
testing.assert_array_equal(
|
||||
safe_as_int([2, 42, 5789234.0, 87, 4]), np.r_[2, 42, 5789234, 87, 4]
|
||||
)
|
||||
testing.assert_array_equal(
|
||||
safe_as_int(
|
||||
np.r_[[[3, 4, 1.000000001], [7, 2, -8.999999999], [6, 9, -4234918347.0]]]
|
||||
),
|
||||
np.r_[[[3, 4, 1], [7, 2, -9], [6, 9, -4234918347]]],
|
||||
)
|
||||
@@ -0,0 +1,154 @@
|
||||
""" Testing decorators module
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import re
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
from numpy.testing import assert_equal
|
||||
from skimage._shared.testing import (
|
||||
doctest_skip_parser,
|
||||
run_in_parallel,
|
||||
assert_stacklevel,
|
||||
)
|
||||
from skimage._shared import testing
|
||||
from skimage._shared._dependency_checks import is_wasm
|
||||
|
||||
from skimage._shared._warnings import expected_warnings
|
||||
from warnings import warn
|
||||
|
||||
|
||||
def test_skipper():
|
||||
def f():
|
||||
pass
|
||||
|
||||
class c:
|
||||
def __init__(self):
|
||||
self.me = "I think, therefore..."
|
||||
|
||||
docstring = """ Header
|
||||
|
||||
>>> something # skip if not HAVE_AMODULE
|
||||
>>> something + else
|
||||
>>> a = 1 # skip if not HAVE_BMODULE
|
||||
>>> something2 # skip if HAVE_AMODULE
|
||||
"""
|
||||
f.__doc__ = docstring
|
||||
c.__doc__ = docstring
|
||||
|
||||
global HAVE_AMODULE, HAVE_BMODULE
|
||||
HAVE_AMODULE = False
|
||||
HAVE_BMODULE = True
|
||||
|
||||
f2 = doctest_skip_parser(f)
|
||||
c2 = doctest_skip_parser(c)
|
||||
assert f is f2
|
||||
assert c is c2
|
||||
|
||||
expected = """ Header
|
||||
|
||||
>>> something # doctest: +SKIP
|
||||
>>> something + else
|
||||
>>> a = 1
|
||||
>>> something2
|
||||
"""
|
||||
assert_equal(f2.__doc__, expected)
|
||||
assert_equal(c2.__doc__, expected)
|
||||
|
||||
HAVE_AMODULE = True
|
||||
HAVE_BMODULE = False
|
||||
f.__doc__ = docstring
|
||||
c.__doc__ = docstring
|
||||
f2 = doctest_skip_parser(f)
|
||||
c2 = doctest_skip_parser(c)
|
||||
|
||||
assert f is f2
|
||||
expected = """ Header
|
||||
|
||||
>>> something
|
||||
>>> something + else
|
||||
>>> a = 1 # doctest: +SKIP
|
||||
>>> something2 # doctest: +SKIP
|
||||
"""
|
||||
assert_equal(f2.__doc__, expected)
|
||||
assert_equal(c2.__doc__, expected)
|
||||
|
||||
del HAVE_AMODULE
|
||||
f.__doc__ = docstring
|
||||
c.__doc__ = docstring
|
||||
with testing.raises(NameError):
|
||||
doctest_skip_parser(f)
|
||||
with testing.raises(NameError):
|
||||
doctest_skip_parser(c)
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_wasm, reason="Cannot start threads in WASM")
|
||||
def test_run_in_parallel():
|
||||
state = []
|
||||
|
||||
@run_in_parallel()
|
||||
def change_state1():
|
||||
state.append(None)
|
||||
|
||||
change_state1()
|
||||
assert len(state) == 2
|
||||
|
||||
@run_in_parallel(num_threads=1)
|
||||
def change_state2():
|
||||
state.append(None)
|
||||
|
||||
change_state2()
|
||||
assert len(state) == 3
|
||||
|
||||
@run_in_parallel(num_threads=3)
|
||||
def change_state3():
|
||||
state.append(None)
|
||||
|
||||
change_state3()
|
||||
assert len(state) == 6
|
||||
|
||||
|
||||
def test_parallel_warning():
|
||||
@run_in_parallel()
|
||||
def change_state_warns_fails():
|
||||
warn("Test warning for test parallel", stacklevel=2)
|
||||
|
||||
with expected_warnings(['Test warning for test parallel']):
|
||||
change_state_warns_fails()
|
||||
|
||||
@run_in_parallel(warnings_matching=['Test warning for test parallel'])
|
||||
def change_state_warns_passes():
|
||||
warn("Test warning for test parallel", stacklevel=2)
|
||||
|
||||
change_state_warns_passes()
|
||||
|
||||
|
||||
def test_expected_warnings_noop():
|
||||
# This will ensure the line beolow it behaves like a no-op
|
||||
with expected_warnings(['Expected warnings test']):
|
||||
# This should behave as a no-op
|
||||
with expected_warnings(None):
|
||||
warn('Expected warnings test')
|
||||
|
||||
|
||||
class Test_assert_stacklevel:
|
||||
def raise_warning(self, *args, **kwargs):
|
||||
warnings.warn(*args, **kwargs)
|
||||
|
||||
def test_correct_stacklevel(self):
|
||||
# Should pass if stacklevel is set correctly
|
||||
with pytest.warns(UserWarning, match="passes") as record:
|
||||
self.raise_warning("passes", UserWarning, stacklevel=2)
|
||||
assert_stacklevel(record)
|
||||
|
||||
@pytest.mark.parametrize("level", [1, 3])
|
||||
def test_wrong_stacklevel(self, level):
|
||||
# AssertionError should be raised for wrong stacklevel
|
||||
with pytest.warns(UserWarning, match="wrong") as record:
|
||||
self.raise_warning("wrong", UserWarning, stacklevel=level)
|
||||
# Check that message contains expected line on right side
|
||||
line_number = inspect.currentframe().f_lineno - 2
|
||||
regex = ".*" + re.escape(f"!= {__file__}:{line_number}")
|
||||
with pytest.raises(AssertionError, match=regex):
|
||||
assert_stacklevel(record, offset=-5)
|
||||
@@ -0,0 +1,516 @@
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from skimage._shared import testing
|
||||
from skimage._shared.utils import (
|
||||
_supported_float_type,
|
||||
_validate_interpolation_order,
|
||||
change_default_value,
|
||||
channel_as_last_axis,
|
||||
check_nD,
|
||||
deprecate_func,
|
||||
deprecate_parameter,
|
||||
DEPRECATED,
|
||||
)
|
||||
|
||||
complex_dtypes = [np.complex64, np.complex128]
|
||||
if hasattr(np, 'complex256'):
|
||||
complex_dtypes += [np.complex256]
|
||||
|
||||
have_numpydoc = False
|
||||
try:
|
||||
import numpydoc # noqa: F401
|
||||
|
||||
have_numpydoc = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def test_change_default_value():
|
||||
@change_default_value('arg1', new_value=-1, changed_version='0.12')
|
||||
def foo(arg0, arg1=0, arg2=1):
|
||||
"""Expected docstring"""
|
||||
return arg0, arg1, arg2
|
||||
|
||||
@change_default_value(
|
||||
'arg1',
|
||||
new_value=-1,
|
||||
changed_version='0.12',
|
||||
warning_msg="Custom warning message",
|
||||
)
|
||||
def bar(arg0, arg1=0, arg2=1):
|
||||
"""Expected docstring"""
|
||||
return arg0, arg1, arg2
|
||||
|
||||
# Assert warning messages
|
||||
with pytest.warns(FutureWarning) as record:
|
||||
assert foo(0) == (0, 0, 1)
|
||||
assert bar(0) == (0, 0, 1)
|
||||
|
||||
expected_msg = (
|
||||
"The new recommended value for arg1 is -1. Until "
|
||||
"version 0.12, the default arg1 value is 0. From "
|
||||
"version 0.12, the arg1 default value will be -1. "
|
||||
"To avoid this warning, please explicitly set arg1 value."
|
||||
)
|
||||
|
||||
assert str(record[0].message) == expected_msg
|
||||
assert str(record[1].message) == "Custom warning message"
|
||||
|
||||
# Assert that nothing happens if arg1 is set
|
||||
with warnings.catch_warnings(record=True) as recorded:
|
||||
# No kwargs
|
||||
assert foo(0, 2) == (0, 2, 1)
|
||||
assert foo(0, arg1=0) == (0, 0, 1)
|
||||
|
||||
# Function name and doc is preserved
|
||||
assert foo.__name__ == 'foo'
|
||||
if sys.flags.optimize < 2:
|
||||
# if PYTHONOPTIMIZE is set to 2, docstrings are stripped
|
||||
assert foo.__doc__ == 'Expected docstring'
|
||||
# Assert no warnings were raised
|
||||
assert len(recorded) == 0
|
||||
|
||||
|
||||
def test_check_nD():
|
||||
z = np.random.random(200**2).reshape((200, 200))
|
||||
x = z[10:30, 30:10]
|
||||
with testing.raises(ValueError):
|
||||
check_nD(x, 2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'dtype', [bool, int, np.uint8, np.uint16, float, np.float32, np.float64]
|
||||
)
|
||||
@pytest.mark.parametrize('order', [None, -1, 0, 1, 2, 3, 4, 5, 6])
|
||||
def test_validate_interpolation_order(dtype, order):
|
||||
if order is None:
|
||||
# Default order
|
||||
assert _validate_interpolation_order(dtype, None) == 0 if dtype == bool else 1
|
||||
elif order < 0 or order > 5:
|
||||
# Order not in valid range
|
||||
with testing.raises(ValueError):
|
||||
_validate_interpolation_order(dtype, order)
|
||||
elif dtype == bool and order != 0:
|
||||
# Deprecated order for bool array
|
||||
with pytest.raises(ValueError):
|
||||
_validate_interpolation_order(bool, order)
|
||||
else:
|
||||
# Valid use case
|
||||
assert _validate_interpolation_order(dtype, order) == order
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'dtype',
|
||||
[
|
||||
bool,
|
||||
np.float16,
|
||||
np.float32,
|
||||
np.float64,
|
||||
np.uint8,
|
||||
np.uint16,
|
||||
np.uint32,
|
||||
np.uint64,
|
||||
np.int8,
|
||||
np.int16,
|
||||
np.int32,
|
||||
np.int64,
|
||||
],
|
||||
)
|
||||
def test_supported_float_dtype_real(dtype):
|
||||
float_dtype = _supported_float_type(dtype)
|
||||
if dtype in [np.float16, np.float32]:
|
||||
assert float_dtype == np.float32
|
||||
else:
|
||||
assert float_dtype == np.float64
|
||||
|
||||
|
||||
@pytest.mark.parametrize('dtype', complex_dtypes)
|
||||
@pytest.mark.parametrize('allow_complex', [False, True])
|
||||
def test_supported_float_dtype_complex(dtype, allow_complex):
|
||||
if allow_complex:
|
||||
float_dtype = _supported_float_type(dtype, allow_complex=allow_complex)
|
||||
if dtype == np.complex64:
|
||||
assert float_dtype == np.complex64
|
||||
else:
|
||||
assert float_dtype == np.complex128
|
||||
else:
|
||||
with testing.raises(ValueError):
|
||||
_supported_float_type(dtype, allow_complex=allow_complex)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('dtype', ['f', 'float32', np.float32, np.dtype(np.float32)])
|
||||
def test_supported_float_dtype_input_kinds(dtype):
|
||||
assert _supported_float_type(dtype) == np.float32
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'dtypes, expected',
|
||||
[
|
||||
((np.float16, np.float64), np.float64),
|
||||
((np.float32, np.uint16, np.int8), np.float64),
|
||||
((np.float32, np.float16), np.float32),
|
||||
],
|
||||
)
|
||||
def test_supported_float_dtype_sequence(dtypes, expected):
|
||||
float_dtype = _supported_float_type(dtypes)
|
||||
assert float_dtype == expected
|
||||
|
||||
|
||||
@channel_as_last_axis(multichannel_output=False)
|
||||
def _decorated_channel_axis_size(x, *, channel_axis=None):
|
||||
if channel_axis is None:
|
||||
return None
|
||||
assert channel_axis == -1
|
||||
return x.shape[-1]
|
||||
|
||||
|
||||
@testing.parametrize('channel_axis', [None, 0, 1, 2, -1, -2, -3])
|
||||
def test_decorated_channel_axis_shape(channel_axis):
|
||||
# Verify that channel_as_last_axis modifies the channel_axis as expected
|
||||
|
||||
# need unique size per axis here
|
||||
x = np.zeros((2, 3, 4))
|
||||
|
||||
size = _decorated_channel_axis_size(x, channel_axis=channel_axis)
|
||||
if channel_axis is None:
|
||||
assert size is None
|
||||
else:
|
||||
assert size == x.shape[channel_axis]
|
||||
|
||||
|
||||
@deprecate_func(
|
||||
deprecated_version="x", removed_version="y", hint="You are on your own."
|
||||
)
|
||||
def _deprecated_func():
|
||||
"""Dummy function used in `test_deprecate_func`.
|
||||
|
||||
The decorated function must be outside the test function, otherwise it
|
||||
seems that the warning does not point at the calling location.
|
||||
"""
|
||||
|
||||
|
||||
def test_deprecate_func():
|
||||
with pytest.warns(FutureWarning) as record:
|
||||
_deprecated_func()
|
||||
testing.assert_stacklevel(record)
|
||||
|
||||
assert len(record) == 1
|
||||
assert record[0].message.args[0] == (
|
||||
"`_deprecated_func` is deprecated since version x and will be removed in "
|
||||
"version y. You are on your own."
|
||||
)
|
||||
|
||||
|
||||
@deprecate_parameter("old1", start_version="0.10", stop_version="0.12")
|
||||
@deprecate_parameter("old0", start_version="0.10", stop_version="0.12")
|
||||
def _func_deprecated_params(arg0, old0=DEPRECATED, old1=DEPRECATED, arg1=None):
|
||||
"""Expected docstring.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arg0 : int
|
||||
First unchanged parameter.
|
||||
arg1 : int, optional
|
||||
Second unchanged parameter.
|
||||
"""
|
||||
return arg0, old0, old1, arg1
|
||||
|
||||
|
||||
@deprecate_parameter("old1", new_name="new0", start_version="0.10", stop_version="0.12")
|
||||
@deprecate_parameter("old0", new_name="new1", start_version="0.10", stop_version="0.12")
|
||||
def _func_replace_params(
|
||||
arg0, old0=DEPRECATED, old1=DEPRECATED, new0=None, new1=None, arg1=None
|
||||
):
|
||||
"""Expected docstring.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arg0 : int
|
||||
First unchanged parameter.
|
||||
new0 : int, optional
|
||||
First new parameter.
|
||||
|
||||
.. versionadded:: 0.10
|
||||
new1 : int, optional
|
||||
Second new parameter.
|
||||
|
||||
.. versionadded:: 0.10
|
||||
arg1 : int, optional
|
||||
Second unchanged parameter.
|
||||
"""
|
||||
return arg0, old0, old1, new0, new1, arg1
|
||||
|
||||
|
||||
class Test_deprecate_parameter:
|
||||
@pytest.mark.skipif(not have_numpydoc, reason="requires numpydoc")
|
||||
def test_docstring_removed_param(self):
|
||||
# function name and doc are preserved
|
||||
assert _func_deprecated_params.__name__ == "_func_deprecated_params"
|
||||
if sys.flags.optimize < 2:
|
||||
# if PYTHONOPTIMIZE is set to 2, docstrings are stripped
|
||||
assert (
|
||||
_func_deprecated_params.__doc__
|
||||
== """Expected docstring.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arg0 : int
|
||||
First unchanged parameter.
|
||||
arg1 : int, optional
|
||||
Second unchanged parameter.
|
||||
|
||||
Other Parameters
|
||||
----------------
|
||||
old0 : DEPRECATED
|
||||
`old0` is deprecated.
|
||||
|
||||
.. deprecated:: 0.10
|
||||
old1 : DEPRECATED
|
||||
`old1` is deprecated.
|
||||
|
||||
.. deprecated:: 0.10
|
||||
"""
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(not have_numpydoc, reason="requires numpydoc")
|
||||
def test_docstring_replaced_param(self):
|
||||
assert _func_replace_params.__name__ == "_func_replace_params"
|
||||
if sys.flags.optimize < 2:
|
||||
# if PYTHONOPTIMIZE is set to 2, docstrings are stripped
|
||||
assert (
|
||||
_func_replace_params.__doc__
|
||||
== """Expected docstring.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arg0 : int
|
||||
First unchanged parameter.
|
||||
new0 : int, optional
|
||||
First new parameter.
|
||||
|
||||
.. versionadded:: 0.10
|
||||
new1 : int, optional
|
||||
Second new parameter.
|
||||
|
||||
.. versionadded:: 0.10
|
||||
arg1 : int, optional
|
||||
Second unchanged parameter.
|
||||
|
||||
Other Parameters
|
||||
----------------
|
||||
old0 : DEPRECATED
|
||||
Deprecated in favor of `new1`.
|
||||
|
||||
.. deprecated:: 0.10
|
||||
old1 : DEPRECATED
|
||||
Deprecated in favor of `new0`.
|
||||
|
||||
.. deprecated:: 0.10
|
||||
"""
|
||||
)
|
||||
|
||||
def test_warning_removed_param(self):
|
||||
match = (
|
||||
r".*`old[01]` is deprecated since version 0\.10 and will be removed "
|
||||
r"in 0\.12.* see the documentation of .*_func_deprecated_params`."
|
||||
)
|
||||
with pytest.warns(FutureWarning, match=match):
|
||||
assert _func_deprecated_params(1, 2) == (1, DEPRECATED, DEPRECATED, None)
|
||||
with pytest.warns(FutureWarning, match=match):
|
||||
assert _func_deprecated_params(1, 2, 3) == (1, DEPRECATED, DEPRECATED, None)
|
||||
with pytest.warns(FutureWarning, match=match):
|
||||
assert _func_deprecated_params(1, old0=2) == (
|
||||
1,
|
||||
DEPRECATED,
|
||||
DEPRECATED,
|
||||
None,
|
||||
)
|
||||
with pytest.warns(FutureWarning, match=match):
|
||||
assert _func_deprecated_params(1, old1=2) == (
|
||||
1,
|
||||
DEPRECATED,
|
||||
DEPRECATED,
|
||||
None,
|
||||
)
|
||||
|
||||
with warnings.catch_warnings(record=True) as record:
|
||||
assert _func_deprecated_params(1, arg1=3) == (1, DEPRECATED, DEPRECATED, 3)
|
||||
assert len(record) == 0
|
||||
|
||||
def test_warning_replaced_param(self):
|
||||
match = (
|
||||
r".*`old[0,1]` is deprecated since version 0\.10 and will be removed "
|
||||
r"in 0\.12.* see the documentation of .*_func_replace_params`."
|
||||
)
|
||||
|
||||
with pytest.warns(FutureWarning, match=match):
|
||||
assert _func_replace_params(1, 2) == (
|
||||
1,
|
||||
DEPRECATED,
|
||||
DEPRECATED,
|
||||
None,
|
||||
2,
|
||||
None,
|
||||
)
|
||||
|
||||
with pytest.warns(FutureWarning, match=match) as records:
|
||||
assert _func_replace_params(1, 2, 3) == (
|
||||
1,
|
||||
DEPRECATED,
|
||||
DEPRECATED,
|
||||
3,
|
||||
2,
|
||||
None,
|
||||
)
|
||||
assert len(records) == 2
|
||||
assert "`old1` is deprecated" in records[0].message.args[0]
|
||||
assert "`old0` is deprecated" in records[1].message.args[0]
|
||||
|
||||
with pytest.warns(FutureWarning, match=match):
|
||||
assert _func_replace_params(1, old0=2) == (
|
||||
1,
|
||||
DEPRECATED,
|
||||
DEPRECATED,
|
||||
None,
|
||||
2,
|
||||
None,
|
||||
)
|
||||
|
||||
with pytest.warns(FutureWarning, match=match):
|
||||
assert _func_replace_params(1, old1=3) == (
|
||||
1,
|
||||
DEPRECATED,
|
||||
DEPRECATED,
|
||||
3,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
# Otherwise, no warnings are emitted!
|
||||
with warnings.catch_warnings(record=True) as record:
|
||||
assert _func_replace_params(1, new0=2, new1=3) == (
|
||||
1,
|
||||
DEPRECATED,
|
||||
DEPRECATED,
|
||||
2,
|
||||
3,
|
||||
None,
|
||||
)
|
||||
assert len(record) == 0
|
||||
|
||||
def test_missing_DEPRECATED(self):
|
||||
decorate = deprecate_parameter(
|
||||
"old", start_version="0.10", stop_version="0.12", stacklevel=2
|
||||
)
|
||||
|
||||
def foo(arg0, old=None):
|
||||
return arg0, old
|
||||
|
||||
with pytest.raises(RuntimeError, match="Expected .* <DEPRECATED>"):
|
||||
decorate(foo)
|
||||
|
||||
def bar(arg0, old=DEPRECATED):
|
||||
return arg0
|
||||
|
||||
assert decorate(bar)(1) == 1
|
||||
|
||||
def test_new_keyword_only(self):
|
||||
@deprecate_parameter(
|
||||
"old",
|
||||
new_name="new",
|
||||
start_version="0.19",
|
||||
stop_version="0.21",
|
||||
)
|
||||
def foo(arg0, old=DEPRECATED, *, new=1, arg3=None):
|
||||
"""Expected docstring"""
|
||||
return arg0, new, arg3
|
||||
|
||||
# Assert that nothing happens when the function is called with the
|
||||
# new API
|
||||
with warnings.catch_warnings(record=True) as recorded:
|
||||
# No kwargs
|
||||
assert foo(0) == (0, 1, None)
|
||||
# Kwargs without deprecated argument
|
||||
assert foo(0, new=1, arg3=2) == (0, 1, 2)
|
||||
assert foo(0, new=2) == (0, 2, None)
|
||||
assert foo(0, arg3=2) == (0, 1, 2)
|
||||
assert len(recorded) == 0
|
||||
|
||||
def test_conflicting_old_and_new(self):
|
||||
match = r".*`old[0,1]` is deprecated"
|
||||
with pytest.warns(FutureWarning, match=match):
|
||||
with pytest.raises(ValueError, match=".* avoid conflicting values"):
|
||||
_func_replace_params(1, old0=2, new1=2)
|
||||
|
||||
with pytest.warns(FutureWarning, match=match):
|
||||
with pytest.raises(ValueError, match=".* avoid conflicting values"):
|
||||
_func_replace_params(1, old1=2, new0=2)
|
||||
|
||||
with pytest.warns(FutureWarning, match=match):
|
||||
with pytest.raises(ValueError, match=".* avoid conflicting values"):
|
||||
_func_replace_params(1, old0=1, old1=1, new0=1, new1=1)
|
||||
|
||||
def test_wrong_call_signature(self):
|
||||
"""Check that normal errors for faulty calls are unchanged."""
|
||||
with pytest.raises(
|
||||
TypeError, match=r".* required positional argument\: 'arg0'"
|
||||
):
|
||||
_func_replace_params()
|
||||
|
||||
with pytest.warns(FutureWarning, match=r".*`old[0,1]` is deprecated"):
|
||||
with pytest.raises(
|
||||
TypeError, match=".* multiple values for argument 'old0'"
|
||||
):
|
||||
_func_deprecated_params(1, 2, old0=2)
|
||||
|
||||
def test_wrong_param_name(self):
|
||||
with pytest.raises(ValueError, match="'old' is not in list"):
|
||||
|
||||
@deprecate_parameter("old", start_version="0.10", stop_version="0.12")
|
||||
def foo(arg0):
|
||||
pass
|
||||
|
||||
with pytest.raises(ValueError, match="'new' is not in list"):
|
||||
|
||||
@deprecate_parameter(
|
||||
"old", new_name="new", start_version="0.10", stop_version="0.12"
|
||||
)
|
||||
def bar(arg0, old, arg1):
|
||||
pass
|
||||
|
||||
def test_warning_location(self):
|
||||
with pytest.warns(FutureWarning) as records:
|
||||
_func_deprecated_params(1, old0=2, old1=2)
|
||||
testing.assert_stacklevel(records)
|
||||
assert len(records) == 2
|
||||
|
||||
def test_stacklevel(self):
|
||||
@deprecate_parameter(
|
||||
"old",
|
||||
start_version="0.19",
|
||||
stop_version="0.21",
|
||||
)
|
||||
def foo(arg0, old=DEPRECATED):
|
||||
pass
|
||||
|
||||
with pytest.raises(RuntimeError, match="Set stacklevel manually"):
|
||||
foo(0, 1)
|
||||
|
||||
@deprecate_parameter(
|
||||
"old",
|
||||
start_version="0.19",
|
||||
stop_version="0.21",
|
||||
stacklevel=2,
|
||||
)
|
||||
def bar(arg0, old=DEPRECATED):
|
||||
pass
|
||||
|
||||
with pytest.warns(FutureWarning, match="`old` is deprecated") as records:
|
||||
bar(0, 1)
|
||||
testing.assert_stacklevel(records)
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
"""Tests for the version requirement functions.
|
||||
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from numpy.testing import assert_equal
|
||||
from skimage._shared import version_requirements as version_req
|
||||
from skimage._shared import testing
|
||||
|
||||
|
||||
def test_get_module_version():
|
||||
assert version_req.get_module_version('numpy')
|
||||
assert version_req.get_module_version('scipy')
|
||||
with testing.raises(ImportError):
|
||||
version_req.get_module_version('fakenumpy')
|
||||
|
||||
|
||||
def test_is_installed():
|
||||
assert version_req.is_installed('python', '>=2.7')
|
||||
assert not version_req.is_installed('numpy', '<1.0')
|
||||
|
||||
|
||||
def test_require():
|
||||
# A function that only runs on Python >2.7 and numpy > 1.5 (should pass)
|
||||
@version_req.require('python', '>2.7')
|
||||
@version_req.require('numpy', '>1.5')
|
||||
def foo():
|
||||
return 1
|
||||
|
||||
assert_equal(foo(), 1)
|
||||
|
||||
# function that requires scipy < 0.1 (should fail)
|
||||
@version_req.require('scipy', '<0.1')
|
||||
def bar():
|
||||
return 0
|
||||
|
||||
with testing.raises(ImportError):
|
||||
bar()
|
||||
|
||||
|
||||
def test_get_module():
|
||||
assert version_req.get_module("numpy") is np
|
||||
@@ -0,0 +1,37 @@
|
||||
import os
|
||||
from skimage._shared._warnings import expected_warnings
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def setup():
|
||||
# Remove any environment variable if it exists
|
||||
old_strictness = os.environ.pop('SKIMAGE_TEST_STRICT_WARNINGS', None)
|
||||
yield
|
||||
# Add the user's desired strictness
|
||||
if old_strictness is not None:
|
||||
os.environ['SKIMAGE_TEST_STRICT_WARNINGS'] = old_strictness
|
||||
|
||||
|
||||
def test_strict_warnigns_default(setup):
|
||||
# By default we should fail on missing expected warnings
|
||||
with pytest.raises(ValueError):
|
||||
with expected_warnings(['some warnings']):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize('strictness', ['1', 'true', 'True', 'TRUE'])
|
||||
def test_strict_warning_true(setup, strictness):
|
||||
os.environ['SKIMAGE_TEST_STRICT_WARNINGS'] = strictness
|
||||
with pytest.raises(ValueError):
|
||||
with expected_warnings(['some warnings']):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize('strictness', ['0', 'false', 'False', 'FALSE'])
|
||||
def test_strict_warning_false(setup, strictness):
|
||||
# If the user doesn't wish to be strict about warnings
|
||||
# the following shouldn't raise any error
|
||||
os.environ['SKIMAGE_TEST_STRICT_WARNINGS'] = strictness
|
||||
with expected_warnings(['some warnings']):
|
||||
pass
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,884 @@
|
||||
import functools
|
||||
import inspect
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ._warnings import all_warnings, warn
|
||||
|
||||
__all__ = [
|
||||
'deprecate_func',
|
||||
'get_bound_method_class',
|
||||
'all_warnings',
|
||||
'safe_as_int',
|
||||
'check_shape_equality',
|
||||
'check_nD',
|
||||
'warn',
|
||||
'reshape_nd',
|
||||
'identity',
|
||||
'slice_at_axis',
|
||||
"deprecate_parameter",
|
||||
"DEPRECATED",
|
||||
]
|
||||
|
||||
|
||||
def _count_wrappers(func):
|
||||
"""Count the number of wrappers around `func`."""
|
||||
unwrapped = func
|
||||
count = 0
|
||||
while hasattr(unwrapped, "__wrapped__"):
|
||||
unwrapped = unwrapped.__wrapped__
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def _warning_stacklevel(func):
|
||||
"""Find stacklevel for a warning raised from a wrapper around `func`.
|
||||
|
||||
Try to determine the number of
|
||||
|
||||
Parameters
|
||||
----------
|
||||
func : Callable
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
stacklevel : int
|
||||
The stacklevel. Minimum of 2.
|
||||
"""
|
||||
# Count number of wrappers around `func`
|
||||
wrapped_count = _count_wrappers(func)
|
||||
|
||||
# Count number of total wrappers around global version of `func`
|
||||
module = sys.modules.get(func.__module__)
|
||||
try:
|
||||
for name in func.__qualname__.split("."):
|
||||
global_func = getattr(module, name)
|
||||
except AttributeError as e:
|
||||
raise RuntimeError(
|
||||
f"Could not access `{func.__qualname__}` in {module!r}, "
|
||||
f" may be a closure. Set stacklevel manually. ",
|
||||
) from e
|
||||
else:
|
||||
global_wrapped_count = _count_wrappers(global_func)
|
||||
|
||||
stacklevel = global_wrapped_count - wrapped_count + 1
|
||||
return max(stacklevel, 2)
|
||||
|
||||
|
||||
def _get_stack_length(func):
|
||||
"""Return function call stack length."""
|
||||
_func = func.__globals__.get(func.__name__, func)
|
||||
length = _count_wrappers(_func)
|
||||
return length
|
||||
|
||||
|
||||
class _DecoratorBaseClass:
|
||||
"""Used to manage decorators' warnings stacklevel.
|
||||
|
||||
The `_stack_length` class variable is used to store the number of
|
||||
times a function is wrapped by a decorator.
|
||||
|
||||
Let `stack_length` be the total number of times a decorated
|
||||
function is wrapped, and `stack_rank` be the rank of the decorator
|
||||
in the decorators stack. The stacklevel of a warning is then
|
||||
`stacklevel = 1 + stack_length - stack_rank`.
|
||||
"""
|
||||
|
||||
_stack_length = {}
|
||||
|
||||
def get_stack_length(self, func):
|
||||
length = self._stack_length.get(func.__name__, _get_stack_length(func))
|
||||
return length
|
||||
|
||||
|
||||
class change_default_value(_DecoratorBaseClass):
|
||||
"""Decorator for changing the default value of an argument.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arg_name: str
|
||||
The name of the argument to be updated.
|
||||
new_value: any
|
||||
The argument new value.
|
||||
changed_version : str
|
||||
The package version in which the change will be introduced.
|
||||
warning_msg: str
|
||||
Optional warning message. If None, a generic warning message
|
||||
is used.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, arg_name, *, new_value, changed_version, warning_msg=None):
|
||||
self.arg_name = arg_name
|
||||
self.new_value = new_value
|
||||
self.warning_msg = warning_msg
|
||||
self.changed_version = changed_version
|
||||
|
||||
def __call__(self, func):
|
||||
parameters = inspect.signature(func).parameters
|
||||
arg_idx = list(parameters.keys()).index(self.arg_name)
|
||||
old_value = parameters[self.arg_name].default
|
||||
|
||||
stack_rank = _count_wrappers(func)
|
||||
|
||||
if self.warning_msg is None:
|
||||
self.warning_msg = (
|
||||
f'The new recommended value for {self.arg_name} is '
|
||||
f'{self.new_value}. Until version {self.changed_version}, '
|
||||
f'the default {self.arg_name} value is {old_value}. '
|
||||
f'From version {self.changed_version}, the {self.arg_name} '
|
||||
f'default value will be {self.new_value}. To avoid '
|
||||
f'this warning, please explicitly set {self.arg_name} value.'
|
||||
)
|
||||
|
||||
@functools.wraps(func)
|
||||
def fixed_func(*args, **kwargs):
|
||||
stacklevel = 1 + self.get_stack_length(func) - stack_rank
|
||||
if len(args) < arg_idx + 1 and self.arg_name not in kwargs.keys():
|
||||
# warn that arg_name default value changed:
|
||||
warnings.warn(self.warning_msg, FutureWarning, stacklevel=stacklevel)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return fixed_func
|
||||
|
||||
|
||||
class PatchClassRepr(type):
|
||||
"""Control class representations in rendered signatures."""
|
||||
|
||||
def __repr__(cls):
|
||||
return f"<{cls.__name__}>"
|
||||
|
||||
|
||||
class DEPRECATED(metaclass=PatchClassRepr):
|
||||
"""Signal value to help with deprecating parameters that use None.
|
||||
|
||||
This is a proxy object, used to signal that a parameter has not been set.
|
||||
This is useful if ``None`` is already used for a different purpose or just
|
||||
to highlight a deprecated parameter in the signature.
|
||||
"""
|
||||
|
||||
|
||||
class deprecate_parameter:
|
||||
"""Deprecate a parameter of a function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
deprecated_name : str
|
||||
The name of the deprecated parameter.
|
||||
start_version : str
|
||||
The package version in which the warning was introduced.
|
||||
stop_version : str
|
||||
The package version in which the warning will be replaced by
|
||||
an error / the deprecation is completed.
|
||||
template : str, optional
|
||||
If given, this message template is used instead of the default one.
|
||||
new_name : str, optional
|
||||
If given, the default message will recommend the new parameter name and an
|
||||
error will be raised if the user uses both old and new names for the
|
||||
same parameter.
|
||||
modify_docstring : bool, optional
|
||||
If the wrapped function has a docstring, add the deprecated parameters
|
||||
to the "Other Parameters" section.
|
||||
stacklevel : int, optional
|
||||
This decorator attempts to detect the appropriate stacklevel for the
|
||||
deprecation warning automatically. If this fails, e.g., due to
|
||||
decorating a closure, you can set the stacklevel manually. The
|
||||
outermost decorator should have stacklevel 2, the next inner one
|
||||
stacklevel 3, etc.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Assign `DEPRECATED` as the new default value for the deprecated parameter.
|
||||
This marks the status of the parameter also in the signature and rendered
|
||||
HTML docs.
|
||||
|
||||
This decorator can be stacked to deprecate more than one parameter.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from skimage._shared.utils import deprecate_parameter, DEPRECATED
|
||||
>>> @deprecate_parameter(
|
||||
... "b", new_name="c", start_version="0.1", stop_version="0.3"
|
||||
... )
|
||||
... def foo(a, b=DEPRECATED, *, c=None):
|
||||
... return a, c
|
||||
|
||||
Calling ``foo(1, b=2)`` will warn with::
|
||||
|
||||
FutureWarning: Parameter `b` is deprecated since version 0.1 and will
|
||||
be removed in 0.3 (or later). To avoid this warning, please use the
|
||||
parameter `c` instead. For more details, see the documentation of
|
||||
`foo`.
|
||||
"""
|
||||
|
||||
DEPRECATED = DEPRECATED # Make signal value accessible for convenience
|
||||
|
||||
remove_parameter_template = (
|
||||
"Parameter `{deprecated_name}` is deprecated since version "
|
||||
"{deprecated_version} and will be removed in {changed_version} (or "
|
||||
"later). To avoid this warning, please do not use the parameter "
|
||||
"`{deprecated_name}`. For more details, see the documentation of "
|
||||
"`{func_name}`."
|
||||
)
|
||||
|
||||
replace_parameter_template = (
|
||||
"Parameter `{deprecated_name}` is deprecated since version "
|
||||
"{deprecated_version} and will be removed in {changed_version} (or "
|
||||
"later). To avoid this warning, please use the parameter `{new_name}` "
|
||||
"instead. For more details, see the documentation of `{func_name}`."
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
deprecated_name,
|
||||
*,
|
||||
start_version,
|
||||
stop_version,
|
||||
template=None,
|
||||
new_name=None,
|
||||
modify_docstring=True,
|
||||
stacklevel=None,
|
||||
):
|
||||
self.deprecated_name = deprecated_name
|
||||
self.new_name = new_name
|
||||
self.template = template
|
||||
self.start_version = start_version
|
||||
self.stop_version = stop_version
|
||||
self.modify_docstring = modify_docstring
|
||||
self.stacklevel = stacklevel
|
||||
|
||||
def __call__(self, func):
|
||||
parameters = inspect.signature(func).parameters
|
||||
deprecated_idx = list(parameters.keys()).index(self.deprecated_name)
|
||||
if self.new_name:
|
||||
new_idx = list(parameters.keys()).index(self.new_name)
|
||||
else:
|
||||
new_idx = False
|
||||
|
||||
if parameters[self.deprecated_name].default is not DEPRECATED:
|
||||
raise RuntimeError(
|
||||
f"Expected `{self.deprecated_name}` to have the value {DEPRECATED!r} "
|
||||
f"to indicate its status in the rendered signature."
|
||||
)
|
||||
|
||||
if self.template is not None:
|
||||
template = self.template
|
||||
elif self.new_name is not None:
|
||||
template = self.replace_parameter_template
|
||||
else:
|
||||
template = self.remove_parameter_template
|
||||
warning_message = template.format(
|
||||
deprecated_name=self.deprecated_name,
|
||||
deprecated_version=self.start_version,
|
||||
changed_version=self.stop_version,
|
||||
func_name=func.__qualname__,
|
||||
new_name=self.new_name,
|
||||
)
|
||||
|
||||
@functools.wraps(func)
|
||||
def fixed_func(*args, **kwargs):
|
||||
deprecated_value = DEPRECATED
|
||||
new_value = DEPRECATED
|
||||
|
||||
# Extract value of deprecated parameter
|
||||
if len(args) > deprecated_idx:
|
||||
deprecated_value = args[deprecated_idx]
|
||||
args = (
|
||||
args[:deprecated_idx] + (DEPRECATED,) + args[deprecated_idx + 1 :]
|
||||
)
|
||||
if self.deprecated_name in kwargs.keys():
|
||||
deprecated_value = kwargs[self.deprecated_name]
|
||||
kwargs[self.deprecated_name] = DEPRECATED
|
||||
# Extract value of new parameter (if present)
|
||||
if new_idx is not False and len(args) > new_idx:
|
||||
new_value = args[new_idx]
|
||||
if self.new_name and self.new_name in kwargs.keys():
|
||||
new_value = kwargs[self.new_name]
|
||||
|
||||
if deprecated_value is not DEPRECATED:
|
||||
stacklevel = (
|
||||
self.stacklevel
|
||||
if self.stacklevel is not None
|
||||
else _warning_stacklevel(func)
|
||||
)
|
||||
warnings.warn(
|
||||
warning_message, category=FutureWarning, stacklevel=stacklevel
|
||||
)
|
||||
|
||||
if new_value is not DEPRECATED:
|
||||
raise ValueError(
|
||||
f"Both deprecated parameter `{self.deprecated_name}` "
|
||||
f"and new parameter `{self.new_name}` are used. Use "
|
||||
f"only the latter to avoid conflicting values."
|
||||
)
|
||||
elif self.new_name is not None:
|
||||
# Assign old value to new one
|
||||
kwargs[self.new_name] = deprecated_value
|
||||
|
||||
return func(*args, **kwargs)
|
||||
|
||||
if self.modify_docstring and func.__doc__ is not None:
|
||||
newdoc = _docstring_add_deprecated(
|
||||
func, {self.deprecated_name: self.new_name}, self.start_version
|
||||
)
|
||||
fixed_func.__doc__ = newdoc
|
||||
|
||||
return fixed_func
|
||||
|
||||
|
||||
def _docstring_add_deprecated(func, kwarg_mapping, deprecated_version):
|
||||
"""Add deprecated kwarg(s) to the "Other Params" section of a docstring.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
func : function
|
||||
The function whose docstring we wish to update.
|
||||
kwarg_mapping : dict
|
||||
A dict containing {old_arg: new_arg} key/value pairs, see
|
||||
`deprecate_parameter`.
|
||||
deprecated_version : str
|
||||
A major.minor version string specifying when old_arg was
|
||||
deprecated.
|
||||
|
||||
Returns
|
||||
-------
|
||||
new_doc : str
|
||||
The updated docstring. Returns the original docstring if numpydoc is
|
||||
not available.
|
||||
"""
|
||||
if func.__doc__ is None:
|
||||
return None
|
||||
try:
|
||||
from numpydoc.docscrape import FunctionDoc, Parameter
|
||||
except ImportError:
|
||||
# Return an unmodified docstring if numpydoc is not available.
|
||||
return func.__doc__
|
||||
|
||||
Doc = FunctionDoc(func)
|
||||
for old_arg, new_arg in kwarg_mapping.items():
|
||||
desc = []
|
||||
if new_arg is None:
|
||||
desc.append(f'`{old_arg}` is deprecated.')
|
||||
else:
|
||||
desc.append(f'Deprecated in favor of `{new_arg}`.')
|
||||
|
||||
desc += ['', f'.. deprecated:: {deprecated_version}']
|
||||
Doc['Other Parameters'].append(
|
||||
Parameter(name=old_arg, type='DEPRECATED', desc=desc)
|
||||
)
|
||||
new_docstring = str(Doc)
|
||||
|
||||
# new_docstring will have a header starting with:
|
||||
#
|
||||
# .. function:: func.__name__
|
||||
#
|
||||
# and some additional blank lines. We strip these off below.
|
||||
split = new_docstring.split('\n')
|
||||
no_header = split[1:]
|
||||
while not no_header[0].strip():
|
||||
no_header.pop(0)
|
||||
|
||||
# Store the initial description before any of the Parameters fields.
|
||||
# Usually this is a single line, but the while loop covers any case
|
||||
# where it is not.
|
||||
descr = no_header.pop(0)
|
||||
while no_header[0].strip():
|
||||
descr += '\n ' + no_header.pop(0)
|
||||
descr += '\n\n'
|
||||
# '\n ' rather than '\n' here to restore the original indentation.
|
||||
final_docstring = descr + '\n '.join(no_header)
|
||||
# strip any extra spaces from ends of lines
|
||||
final_docstring = '\n'.join([line.rstrip() for line in final_docstring.split('\n')])
|
||||
return final_docstring
|
||||
|
||||
|
||||
class channel_as_last_axis:
|
||||
"""Decorator for automatically making channels axis last for all arrays.
|
||||
|
||||
This decorator reorders axes for compatibility with functions that only
|
||||
support channels along the last axis. After the function call is complete
|
||||
the channels axis is restored back to its original position.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
channel_arg_positions : tuple of int, optional
|
||||
Positional arguments at the positions specified in this tuple are
|
||||
assumed to be multichannel arrays. The default is to assume only the
|
||||
first argument to the function is a multichannel array.
|
||||
channel_kwarg_names : tuple of str, optional
|
||||
A tuple containing the names of any keyword arguments corresponding to
|
||||
multichannel arrays.
|
||||
multichannel_output : bool, optional
|
||||
A boolean that should be True if the output of the function is not a
|
||||
multichannel array and False otherwise. This decorator does not
|
||||
currently support the general case of functions with multiple outputs
|
||||
where some or all are multichannel.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
channel_arg_positions=(0,),
|
||||
channel_kwarg_names=(),
|
||||
multichannel_output=True,
|
||||
):
|
||||
self.arg_positions = set(channel_arg_positions)
|
||||
self.kwarg_names = set(channel_kwarg_names)
|
||||
self.multichannel_output = multichannel_output
|
||||
|
||||
def __call__(self, func):
|
||||
@functools.wraps(func)
|
||||
def fixed_func(*args, **kwargs):
|
||||
channel_axis = kwargs.get('channel_axis', None)
|
||||
|
||||
if channel_axis is None:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
# TODO: convert scalars to a tuple in anticipation of eventually
|
||||
# supporting a tuple of channel axes. Right now, only an
|
||||
# integer or a single-element tuple is supported, though.
|
||||
if np.isscalar(channel_axis):
|
||||
channel_axis = (channel_axis,)
|
||||
if len(channel_axis) > 1:
|
||||
raise ValueError("only a single channel axis is currently supported")
|
||||
|
||||
if channel_axis == (-1,) or channel_axis == -1:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
if self.arg_positions:
|
||||
new_args = []
|
||||
for pos, arg in enumerate(args):
|
||||
if pos in self.arg_positions:
|
||||
new_args.append(np.moveaxis(arg, channel_axis[0], -1))
|
||||
else:
|
||||
new_args.append(arg)
|
||||
new_args = tuple(new_args)
|
||||
else:
|
||||
new_args = args
|
||||
|
||||
for name in self.kwarg_names:
|
||||
kwargs[name] = np.moveaxis(kwargs[name], channel_axis[0], -1)
|
||||
|
||||
# now that we have moved the channels axis to the last position,
|
||||
# change the channel_axis argument to -1
|
||||
kwargs["channel_axis"] = -1
|
||||
|
||||
# Call the function with the fixed arguments
|
||||
out = func(*new_args, **kwargs)
|
||||
if self.multichannel_output:
|
||||
out = np.moveaxis(out, -1, channel_axis[0])
|
||||
return out
|
||||
|
||||
return fixed_func
|
||||
|
||||
|
||||
class deprecate_func(_DecoratorBaseClass):
|
||||
"""Decorate a deprecated function and warn when it is called.
|
||||
|
||||
Adapted from <http://wiki.python.org/moin/PythonDecoratorLibrary>.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
deprecated_version : str
|
||||
The package version when the deprecation was introduced.
|
||||
removed_version : str
|
||||
The package version in which the deprecated function will be removed.
|
||||
hint : str, optional
|
||||
A hint on how to address this deprecation,
|
||||
e.g., "Use `skimage.submodule.alternative_func` instead."
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> @deprecate_func(
|
||||
... deprecated_version="1.0.0",
|
||||
... removed_version="1.2.0",
|
||||
... hint="Use `bar` instead."
|
||||
... )
|
||||
... def foo():
|
||||
... pass
|
||||
|
||||
Calling ``foo`` will warn with::
|
||||
|
||||
FutureWarning: `foo` is deprecated since version 1.0.0
|
||||
and will be removed in version 1.2.0. Use `bar` instead.
|
||||
"""
|
||||
|
||||
def __init__(self, *, deprecated_version, removed_version=None, hint=None):
|
||||
self.deprecated_version = deprecated_version
|
||||
self.removed_version = removed_version
|
||||
self.hint = hint
|
||||
|
||||
def __call__(self, func):
|
||||
message = (
|
||||
f"`{func.__name__}` is deprecated since version "
|
||||
f"{self.deprecated_version}"
|
||||
)
|
||||
if self.removed_version:
|
||||
message += f" and will be removed in version {self.removed_version}."
|
||||
if self.hint:
|
||||
# Prepend space and make sure it closes with "."
|
||||
message += f" {self.hint.rstrip('.')}."
|
||||
|
||||
stack_rank = _count_wrappers(func)
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
stacklevel = 1 + self.get_stack_length(func) - stack_rank
|
||||
warnings.warn(message, category=FutureWarning, stacklevel=stacklevel)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
# modify docstring to display deprecation warning
|
||||
doc = f'**Deprecated:** {message}'
|
||||
if wrapped.__doc__ is None:
|
||||
wrapped.__doc__ = doc
|
||||
else:
|
||||
wrapped.__doc__ = doc + '\n\n ' + wrapped.__doc__
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def get_bound_method_class(m):
|
||||
"""Return the class for a bound method."""
|
||||
return m.im_class if sys.version < '3' else m.__self__.__class__
|
||||
|
||||
|
||||
def safe_as_int(val, atol=1e-3):
|
||||
"""
|
||||
Attempt to safely cast values to integer format.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
val : scalar or iterable of scalars
|
||||
Number or container of numbers which are intended to be interpreted as
|
||||
integers, e.g., for indexing purposes, but which may not carry integer
|
||||
type.
|
||||
atol : float
|
||||
Absolute tolerance away from nearest integer to consider values in
|
||||
``val`` functionally integers.
|
||||
|
||||
Returns
|
||||
-------
|
||||
val_int : NumPy scalar or ndarray of dtype `np.int64`
|
||||
Returns the input value(s) coerced to dtype `np.int64` assuming all
|
||||
were within ``atol`` of the nearest integer.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This operation calculates ``val`` modulo 1, which returns the mantissa of
|
||||
all values. Then all mantissas greater than 0.5 are subtracted from one.
|
||||
Finally, the absolute tolerance from zero is calculated. If it is less
|
||||
than ``atol`` for all value(s) in ``val``, they are rounded and returned
|
||||
in an integer array. Or, if ``val`` was a scalar, a NumPy scalar type is
|
||||
returned.
|
||||
|
||||
If any value(s) are outside the specified tolerance, an informative error
|
||||
is raised.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> safe_as_int(7.0)
|
||||
7
|
||||
|
||||
>>> safe_as_int([9, 4, 2.9999999999])
|
||||
array([9, 4, 3])
|
||||
|
||||
>>> safe_as_int(53.1)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: Integer argument required but received 53.1, check inputs.
|
||||
|
||||
>>> safe_as_int(53.01, atol=0.01)
|
||||
53
|
||||
|
||||
"""
|
||||
mod = np.asarray(val) % 1 # Extract mantissa
|
||||
|
||||
# Check for and subtract any mod values > 0.5 from 1
|
||||
if mod.ndim == 0: # Scalar input, cannot be indexed
|
||||
if mod > 0.5:
|
||||
mod = 1 - mod
|
||||
else: # Iterable input, now ndarray
|
||||
mod[mod > 0.5] = 1 - mod[mod > 0.5] # Test on each side of nearest int
|
||||
|
||||
try:
|
||||
np.testing.assert_allclose(mod, 0, atol=atol)
|
||||
except AssertionError:
|
||||
raise ValueError(
|
||||
f'Integer argument required but received ' f'{val}, check inputs.'
|
||||
)
|
||||
|
||||
return np.round(val).astype(np.int64)
|
||||
|
||||
|
||||
def check_shape_equality(*images):
|
||||
"""Check that all images have the same shape"""
|
||||
image0 = images[0]
|
||||
if not all(image0.shape == image.shape for image in images[1:]):
|
||||
raise ValueError('Input images must have the same dimensions.')
|
||||
return
|
||||
|
||||
|
||||
def slice_at_axis(sl, axis):
|
||||
"""
|
||||
Construct tuple of slices to slice an array in the given dimension.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sl : slice
|
||||
The slice for the given dimension.
|
||||
axis : int
|
||||
The axis to which `sl` is applied. All other dimensions are left
|
||||
"unsliced".
|
||||
|
||||
Returns
|
||||
-------
|
||||
sl : tuple of slices
|
||||
A tuple with slices matching `shape` in length.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> slice_at_axis(slice(None, 3, -1), 1)
|
||||
(slice(None, None, None), slice(None, 3, -1), Ellipsis)
|
||||
"""
|
||||
return (slice(None),) * axis + (sl,) + (...,)
|
||||
|
||||
|
||||
def reshape_nd(arr, ndim, dim):
|
||||
"""Reshape a 1D array to have n dimensions, all singletons but one.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arr : array, shape (N,)
|
||||
Input array
|
||||
ndim : int
|
||||
Number of desired dimensions of reshaped array.
|
||||
dim : int
|
||||
Which dimension/axis will not be singleton-sized.
|
||||
|
||||
Returns
|
||||
-------
|
||||
arr_reshaped : array, shape ([1, ...], N, [1,...])
|
||||
View of `arr` reshaped to the desired shape.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> rng = np.random.default_rng()
|
||||
>>> arr = rng.random(7)
|
||||
>>> reshape_nd(arr, 2, 0).shape
|
||||
(7, 1)
|
||||
>>> reshape_nd(arr, 3, 1).shape
|
||||
(1, 7, 1)
|
||||
>>> reshape_nd(arr, 4, -1).shape
|
||||
(1, 1, 1, 7)
|
||||
"""
|
||||
if arr.ndim != 1:
|
||||
raise ValueError("arr must be a 1D array")
|
||||
new_shape = [1] * ndim
|
||||
new_shape[dim] = -1
|
||||
return np.reshape(arr, new_shape)
|
||||
|
||||
|
||||
def check_nD(array, ndim, arg_name='image'):
|
||||
"""
|
||||
Verify an array meets the desired ndims and array isn't empty.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
array : array-like
|
||||
Input array to be validated
|
||||
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_incorrect_dim = "The parameter `%s` must be a %s-dimensional array"
|
||||
msg_empty_array = "The parameter `%s` cannot be an empty array"
|
||||
if isinstance(ndim, int):
|
||||
ndim = [ndim]
|
||||
if array.size == 0:
|
||||
raise ValueError(msg_empty_array % (arg_name))
|
||||
if array.ndim not in ndim:
|
||||
raise ValueError(
|
||||
msg_incorrect_dim % (arg_name, '-or-'.join([str(n) for n in ndim]))
|
||||
)
|
||||
|
||||
|
||||
def convert_to_float(image, preserve_range):
|
||||
"""Convert input image to float image with the appropriate range.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image : ndarray
|
||||
Input image.
|
||||
preserve_range : bool
|
||||
Determines if the range of the image should be kept or transformed
|
||||
using img_as_float. Also see
|
||||
https://scikit-image.org/docs/dev/user_guide/data_types.html
|
||||
|
||||
Notes
|
||||
-----
|
||||
* Input images with `float32` data type are not upcast.
|
||||
|
||||
Returns
|
||||
-------
|
||||
image : ndarray
|
||||
Transformed version of the input.
|
||||
|
||||
"""
|
||||
if image.dtype == np.float16:
|
||||
return image.astype(np.float32)
|
||||
if preserve_range:
|
||||
# Convert image to double only if it is not single or double
|
||||
# precision float
|
||||
if image.dtype.char not in 'df':
|
||||
image = image.astype(float)
|
||||
else:
|
||||
from ..util.dtype import img_as_float
|
||||
|
||||
image = img_as_float(image)
|
||||
return image
|
||||
|
||||
|
||||
def _validate_interpolation_order(image_dtype, order):
|
||||
"""Validate and return spline interpolation's order.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image_dtype : dtype
|
||||
Image dtype.
|
||||
order : int, optional
|
||||
The order of the spline interpolation. The order has to be in
|
||||
the range 0-5. See `skimage.transform.warp` for detail.
|
||||
|
||||
Returns
|
||||
-------
|
||||
order : int
|
||||
if input order is None, returns 0 if image_dtype is bool and 1
|
||||
otherwise. Otherwise, image_dtype is checked and input order
|
||||
is validated accordingly (order > 0 is not supported for bool
|
||||
image dtype)
|
||||
|
||||
"""
|
||||
|
||||
if order is None:
|
||||
return 0 if image_dtype == bool else 1
|
||||
|
||||
if order < 0 or order > 5:
|
||||
raise ValueError("Spline interpolation order has to be in the " "range 0-5.")
|
||||
|
||||
if image_dtype == bool and order != 0:
|
||||
raise ValueError(
|
||||
"Input image dtype is bool. Interpolation is not defined "
|
||||
"with bool data type. Please set order to 0 or explicitly "
|
||||
"cast input image to another data type."
|
||||
)
|
||||
|
||||
return order
|
||||
|
||||
|
||||
def _to_np_mode(mode):
|
||||
"""Convert padding modes from `ndi.correlate` to `np.pad`."""
|
||||
mode_translation_dict = dict(nearest='edge', reflect='symmetric', mirror='reflect')
|
||||
if mode in mode_translation_dict:
|
||||
mode = mode_translation_dict[mode]
|
||||
return mode
|
||||
|
||||
|
||||
def _to_ndimage_mode(mode):
|
||||
"""Convert from `numpy.pad` mode name to the corresponding ndimage mode."""
|
||||
mode_translation_dict = dict(
|
||||
constant='constant',
|
||||
edge='nearest',
|
||||
symmetric='reflect',
|
||||
reflect='mirror',
|
||||
wrap='wrap',
|
||||
)
|
||||
if mode not in mode_translation_dict:
|
||||
raise ValueError(
|
||||
f"Unknown mode: '{mode}', or cannot translate mode. The "
|
||||
f"mode should be one of 'constant', 'edge', 'symmetric', "
|
||||
f"'reflect', or 'wrap'. See the documentation of numpy.pad for "
|
||||
f"more info."
|
||||
)
|
||||
return _fix_ndimage_mode(mode_translation_dict[mode])
|
||||
|
||||
|
||||
def _fix_ndimage_mode(mode):
|
||||
# SciPy 1.6.0 introduced grid variants of constant and wrap which
|
||||
# have less surprising behavior for images. Use these when available
|
||||
grid_modes = {'constant': 'grid-constant', 'wrap': 'grid-wrap'}
|
||||
return grid_modes.get(mode, mode)
|
||||
|
||||
|
||||
new_float_type = {
|
||||
# preserved types
|
||||
np.float32().dtype.char: np.float32,
|
||||
np.float64().dtype.char: np.float64,
|
||||
np.complex64().dtype.char: np.complex64,
|
||||
np.complex128().dtype.char: np.complex128,
|
||||
# altered types
|
||||
np.float16().dtype.char: np.float32,
|
||||
'g': np.float64, # np.float128 ; doesn't exist on windows
|
||||
'G': np.complex128, # np.complex256 ; doesn't exist on windows
|
||||
}
|
||||
|
||||
|
||||
def _supported_float_type(input_dtype, allow_complex=False):
|
||||
"""Return an appropriate floating-point dtype for a given dtype.
|
||||
|
||||
float32, float64, complex64, complex128 are preserved.
|
||||
float16 is promoted to float32.
|
||||
complex256 is demoted to complex128.
|
||||
Other types are cast to float64.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input_dtype : np.dtype or tuple of np.dtype
|
||||
The input dtype. If a tuple of multiple dtypes is provided, each
|
||||
dtype is first converted to a supported floating point type and the
|
||||
final dtype is then determined by applying `np.result_type` on the
|
||||
sequence of supported floating point types.
|
||||
allow_complex : bool, optional
|
||||
If False, raise a ValueError on complex-valued inputs.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float_type : dtype
|
||||
Floating-point dtype for the image.
|
||||
"""
|
||||
if isinstance(input_dtype, tuple):
|
||||
return np.result_type(*(_supported_float_type(d) for d in input_dtype))
|
||||
input_dtype = np.dtype(input_dtype)
|
||||
if not allow_complex and input_dtype.kind == 'c':
|
||||
raise ValueError("complex valued input is not supported")
|
||||
return new_float_type.get(input_dtype.char, np.float64)
|
||||
|
||||
|
||||
def identity(image, *args, **kwargs):
|
||||
"""Returns the first argument unmodified."""
|
||||
return image
|
||||
|
||||
|
||||
def as_binary_ndarray(array, *, variable_name):
|
||||
"""Return `array` as a numpy.ndarray of dtype bool.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError:
|
||||
An error including the given `variable_name` if `array` can not be
|
||||
safely cast to a boolean array.
|
||||
"""
|
||||
array = np.asarray(array)
|
||||
if array.dtype != bool:
|
||||
if np.any((array != 1) & (array != 0)):
|
||||
raise ValueError(
|
||||
f"{variable_name} array is not of dtype boolean or "
|
||||
f"contains values other than 0 and 1 so cannot be "
|
||||
f"safely cast to boolean array."
|
||||
)
|
||||
return np.asarray(array, dtype=bool)
|
||||
@@ -0,0 +1,138 @@
|
||||
import sys
|
||||
|
||||
from packaging import version as _version
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
try:
|
||||
if cmp_op == '>':
|
||||
return _version.parse(actver) > _version.parse(version)
|
||||
elif cmp_op == '>=':
|
||||
return _version.parse(actver) >= _version.parse(version)
|
||||
elif cmp_op == '=':
|
||||
return _version.parse(actver) == _version.parse(version)
|
||||
elif cmp_op == '<':
|
||||
return _version.parse(actver) < _version.parse(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
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : bool
|
||||
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:
|
||||
# since version_requirements is in the critical import path,
|
||||
# we lazy import re
|
||||
import re
|
||||
|
||||
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 ('>=', '>', '=', '<'), f"Invalid version condition '{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
|
||||
|
||||
Returns
|
||||
-------
|
||||
func : function
|
||||
A decorator that raises an ImportError if a function is run
|
||||
in the absence of the input dependency.
|
||||
"""
|
||||
# since version_requirements is in the critical import path, we lazy import
|
||||
# functools
|
||||
import functools
|
||||
|
||||
def decorator(obj):
|
||||
@functools.wraps(obj)
|
||||
def func_wrapped(*args, **kwargs):
|
||||
if is_installed(name, version):
|
||||
return obj(*args, **kwargs)
|
||||
else:
|
||||
msg = f'"{obj}" in "{obj.__module__}" requires "{name}'
|
||||
if version is not None:
|
||||
msg += f" {version}"
|
||||
raise ImportError(msg + '"')
|
||||
|
||||
return func_wrapped
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
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
|
||||
-------
|
||||
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
|
||||
return __import__(module_name, fromlist=[module_name.rpartition('.')[-1]])
|
||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,293 @@
|
||||
# Vendored subset of numpy/lib/utils.py in 1.26.3
|
||||
# https://github.com/numpy/numpy/blob/b4bf93b936802618ebb49ee43e382b576b29a0a6/numpy/lib/utils.py
|
||||
#
|
||||
# Can be removed after deprecation of `skimage.lookfor` is completed.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
from numpy import ufunc
|
||||
|
||||
|
||||
# Cache for lookfor: {id(module): {name: (docstring, kind, index), ...}...}
|
||||
# where kind: "func", "class", "module", "object"
|
||||
# and index: index in breadth-first namespace traversal
|
||||
_lookfor_caches = {}
|
||||
|
||||
|
||||
# regexp whose match indicates that the string may contain a function
|
||||
# signature
|
||||
_function_signature_re = re.compile(r"[a-z0-9_]+\(.*[,=].*\)", re.I)
|
||||
|
||||
|
||||
def _getmembers(item):
|
||||
import inspect
|
||||
|
||||
try:
|
||||
members = inspect.getmembers(item)
|
||||
except Exception:
|
||||
members = [(x, getattr(item, x)) for x in dir(item) if hasattr(item, x)]
|
||||
return members
|
||||
|
||||
|
||||
def _lookfor_generate_cache(module, import_modules, regenerate):
|
||||
"""
|
||||
Generate docstring cache for given module.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
module : str, None, module
|
||||
Module for which to generate docstring cache
|
||||
import_modules : bool
|
||||
Whether to import sub-modules in packages.
|
||||
regenerate : bool
|
||||
Re-generate the docstring cache
|
||||
|
||||
Returns
|
||||
-------
|
||||
cache : dict {obj_full_name: (docstring, kind, index), ...}
|
||||
Docstring cache for the module, either cached one (regenerate=False)
|
||||
or newly generated.
|
||||
|
||||
"""
|
||||
# Local import to speed up numpy's import time.
|
||||
import inspect
|
||||
|
||||
from io import StringIO
|
||||
|
||||
if module is None:
|
||||
module = "numpy"
|
||||
|
||||
if isinstance(module, str):
|
||||
try:
|
||||
__import__(module)
|
||||
except ImportError:
|
||||
return {}
|
||||
module = sys.modules[module]
|
||||
elif isinstance(module, list) or isinstance(module, tuple):
|
||||
cache = {}
|
||||
for mod in module:
|
||||
cache.update(_lookfor_generate_cache(mod, import_modules, regenerate))
|
||||
return cache
|
||||
|
||||
if id(module) in _lookfor_caches and not regenerate:
|
||||
return _lookfor_caches[id(module)]
|
||||
|
||||
# walk items and collect docstrings
|
||||
cache = {}
|
||||
_lookfor_caches[id(module)] = cache
|
||||
seen = {}
|
||||
index = 0
|
||||
stack = [(module.__name__, module)]
|
||||
while stack:
|
||||
name, item = stack.pop(0)
|
||||
if id(item) in seen:
|
||||
continue
|
||||
seen[id(item)] = True
|
||||
|
||||
index += 1
|
||||
kind = "object"
|
||||
|
||||
if inspect.ismodule(item):
|
||||
kind = "module"
|
||||
try:
|
||||
_all = item.__all__
|
||||
except AttributeError:
|
||||
_all = None
|
||||
|
||||
# import sub-packages
|
||||
if import_modules and hasattr(item, '__path__'):
|
||||
for pth in item.__path__:
|
||||
for mod_path in os.listdir(pth):
|
||||
this_py = os.path.join(pth, mod_path)
|
||||
init_py = os.path.join(pth, mod_path, '__init__.py')
|
||||
if os.path.isfile(this_py) and mod_path.endswith('.py'):
|
||||
to_import = mod_path[:-3]
|
||||
elif os.path.isfile(init_py):
|
||||
to_import = mod_path
|
||||
else:
|
||||
continue
|
||||
if to_import == '__init__':
|
||||
continue
|
||||
|
||||
try:
|
||||
old_stdout = sys.stdout
|
||||
old_stderr = sys.stderr
|
||||
try:
|
||||
sys.stdout = StringIO()
|
||||
sys.stderr = StringIO()
|
||||
__import__("%s.%s" % (name, to_import)) # noqa: UP031
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
sys.stderr = old_stderr
|
||||
except KeyboardInterrupt:
|
||||
# Assume keyboard interrupt came from a user
|
||||
raise
|
||||
except BaseException:
|
||||
# Ignore also SystemExit and pytests.importorskip
|
||||
# `Skipped` (these are BaseExceptions; gh-22345)
|
||||
continue
|
||||
|
||||
for n, v in _getmembers(item):
|
||||
try:
|
||||
item_name = getattr(
|
||||
v, '__name__', "%s.%s" % (name, n) # noqa: UP031
|
||||
) # noqa: UP031
|
||||
mod_name = getattr(v, '__module__', None)
|
||||
except NameError:
|
||||
# ref. SWIG's global cvars
|
||||
# NameError: Unknown C global variable
|
||||
item_name = "%s.%s" % (name, n) # noqa: UP031
|
||||
mod_name = None
|
||||
if '.' not in item_name and mod_name:
|
||||
item_name = "%s.%s" % (mod_name, item_name) # noqa: UP031
|
||||
|
||||
if not item_name.startswith(name + '.'):
|
||||
# don't crawl "foreign" objects
|
||||
if isinstance(v, ufunc):
|
||||
# ... unless they are ufuncs
|
||||
pass
|
||||
else:
|
||||
continue
|
||||
elif not (inspect.ismodule(v) or _all is None or n in _all):
|
||||
continue
|
||||
stack.append(("%s.%s" % (name, n), v)) # noqa: UP031
|
||||
elif inspect.isclass(item):
|
||||
kind = "class"
|
||||
for n, v in _getmembers(item):
|
||||
stack.append(("%s.%s" % (name, n), v)) # noqa: UP031
|
||||
elif hasattr(item, "__call__"):
|
||||
kind = "func"
|
||||
|
||||
try:
|
||||
doc = inspect.getdoc(item)
|
||||
except NameError:
|
||||
# ref SWIG's NameError: Unknown C global variable
|
||||
doc = None
|
||||
if doc is not None:
|
||||
cache[name] = (doc, kind, index)
|
||||
|
||||
return cache
|
||||
|
||||
|
||||
def lookfor(what, module=None, import_modules=True, regenerate=False, output=None):
|
||||
"""
|
||||
Do a keyword search on docstrings.
|
||||
|
||||
A list of objects that matched the search is displayed,
|
||||
sorted by relevance. All given keywords need to be found in the
|
||||
docstring for it to be returned as a result, but the order does
|
||||
not matter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
what : str
|
||||
String containing words to look for.
|
||||
module : str or list, optional
|
||||
Name of module(s) whose docstrings to go through.
|
||||
import_modules : bool, optional
|
||||
Whether to import sub-modules in packages. Default is True.
|
||||
regenerate : bool, optional
|
||||
Whether to re-generate the docstring cache. Default is False.
|
||||
output : file-like, optional
|
||||
File-like object to write the output to. If omitted, use a pager.
|
||||
|
||||
See Also
|
||||
--------
|
||||
source, info
|
||||
|
||||
Notes
|
||||
-----
|
||||
Relevance is determined only roughly, by checking if the keywords occur
|
||||
in the function name, at the start of a docstring, etc.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> np.lookfor('binary representation') # doctest: +SKIP
|
||||
Search results for 'binary representation'
|
||||
------------------------------------------
|
||||
numpy.binary_repr
|
||||
Return the binary representation of the input number as a string.
|
||||
numpy.core.setup_common.long_double_representation
|
||||
Given a binary dump as given by GNU od -b, look for long double
|
||||
numpy.base_repr
|
||||
Return a string representation of a number in the given base system.
|
||||
...
|
||||
|
||||
"""
|
||||
import pydoc
|
||||
|
||||
# Cache
|
||||
cache = _lookfor_generate_cache(module, import_modules, regenerate)
|
||||
|
||||
# Search
|
||||
# XXX: maybe using a real stemming search engine would be better?
|
||||
found = []
|
||||
whats = str(what).lower().split()
|
||||
if not whats:
|
||||
return
|
||||
|
||||
for name, (docstring, kind, index) in cache.items():
|
||||
if kind in ('module', 'object'):
|
||||
# don't show modules or objects
|
||||
continue
|
||||
doc = docstring.lower()
|
||||
if all(w in doc for w in whats):
|
||||
found.append(name)
|
||||
|
||||
# Relevance sort
|
||||
# XXX: this is full Harrison-Stetson heuristics now,
|
||||
# XXX: it probably could be improved
|
||||
|
||||
kind_relevance = {'func': 1000, 'class': 1000, 'module': -1000, 'object': -1000}
|
||||
|
||||
def relevance(name, docstr, kind, index):
|
||||
r = 0
|
||||
# do the keywords occur within the start of the docstring?
|
||||
first_doc = "\n".join(docstr.lower().strip().split("\n")[:3])
|
||||
r += sum([200 for w in whats if w in first_doc])
|
||||
# do the keywords occur in the function name?
|
||||
r += sum([30 for w in whats if w in name])
|
||||
# is the full name long?
|
||||
r += -len(name) * 5
|
||||
# is the object of bad type?
|
||||
r += kind_relevance.get(kind, -1000)
|
||||
# is the object deep in namespace hierarchy?
|
||||
r += -name.count('.') * 10
|
||||
r += max(-index / 100, -100)
|
||||
return r
|
||||
|
||||
def relevance_value(a):
|
||||
return relevance(a, *cache[a])
|
||||
|
||||
found.sort(key=relevance_value)
|
||||
|
||||
# Pretty-print
|
||||
s = "Search results for '%s'" % (' '.join(whats))
|
||||
help_text = [s, "-" * len(s)]
|
||||
for name in found[::-1]:
|
||||
doc, kind, ix = cache[name]
|
||||
|
||||
doclines = [line.strip() for line in doc.strip().split("\n") if line.strip()]
|
||||
|
||||
# find a suitable short description
|
||||
try:
|
||||
first_doc = doclines[0].strip()
|
||||
if _function_signature_re.search(first_doc):
|
||||
first_doc = doclines[1].strip()
|
||||
except IndexError:
|
||||
first_doc = ""
|
||||
help_text.append("%s\n %s" % (name, first_doc)) # noqa: UP031
|
||||
|
||||
if not found:
|
||||
help_text.append("Nothing found.")
|
||||
|
||||
# Output
|
||||
if output is not None:
|
||||
output.write("\n".join(help_text))
|
||||
elif len(help_text) > 10:
|
||||
pager = pydoc.getpager()
|
||||
pager("\n".join(help_text))
|
||||
else:
|
||||
print("\n".join(help_text))
|
||||
@@ -0,0 +1,3 @@
|
||||
import lazy_loader as lazy
|
||||
|
||||
__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__)
|
||||
@@ -0,0 +1,135 @@
|
||||
# Explicitly setting `__all__` is necessary for type inference engines
|
||||
# to know which symbols are exported. See
|
||||
# https://peps.python.org/pep-0484/#stub-files
|
||||
|
||||
__all__ = [
|
||||
'convert_colorspace',
|
||||
'xyz_tristimulus_values',
|
||||
'rgba2rgb',
|
||||
'rgb2hsv',
|
||||
'hsv2rgb',
|
||||
'rgb2xyz',
|
||||
'xyz2rgb',
|
||||
'rgb2rgbcie',
|
||||
'rgbcie2rgb',
|
||||
'rgb2gray',
|
||||
'gray2rgb',
|
||||
'gray2rgba',
|
||||
'xyz2lab',
|
||||
'lab2xyz',
|
||||
'lab2rgb',
|
||||
'rgb2lab',
|
||||
'rgb2hed',
|
||||
'hed2rgb',
|
||||
'lab2lch',
|
||||
'lch2lab',
|
||||
'rgb2yuv',
|
||||
'yuv2rgb',
|
||||
'rgb2yiq',
|
||||
'yiq2rgb',
|
||||
'rgb2ypbpr',
|
||||
'ypbpr2rgb',
|
||||
'rgb2ycbcr',
|
||||
'ycbcr2rgb',
|
||||
'rgb2ydbdr',
|
||||
'ydbdr2rgb',
|
||||
'separate_stains',
|
||||
'combine_stains',
|
||||
'rgb_from_hed',
|
||||
'hed_from_rgb',
|
||||
'rgb_from_hdx',
|
||||
'hdx_from_rgb',
|
||||
'rgb_from_fgx',
|
||||
'fgx_from_rgb',
|
||||
'rgb_from_bex',
|
||||
'bex_from_rgb',
|
||||
'rgb_from_rbd',
|
||||
'rbd_from_rgb',
|
||||
'rgb_from_gdx',
|
||||
'gdx_from_rgb',
|
||||
'rgb_from_hax',
|
||||
'hax_from_rgb',
|
||||
'rgb_from_bro',
|
||||
'bro_from_rgb',
|
||||
'rgb_from_bpx',
|
||||
'bpx_from_rgb',
|
||||
'rgb_from_ahx',
|
||||
'ahx_from_rgb',
|
||||
'rgb_from_hpx',
|
||||
'hpx_from_rgb',
|
||||
'color_dict',
|
||||
'label2rgb',
|
||||
'deltaE_cie76',
|
||||
'deltaE_ciede94',
|
||||
'deltaE_ciede2000',
|
||||
'deltaE_cmc',
|
||||
]
|
||||
|
||||
from .colorconv import (
|
||||
convert_colorspace,
|
||||
xyz_tristimulus_values,
|
||||
rgba2rgb,
|
||||
rgb2hsv,
|
||||
hsv2rgb,
|
||||
rgb2xyz,
|
||||
xyz2rgb,
|
||||
rgb2rgbcie,
|
||||
rgbcie2rgb,
|
||||
rgb2gray,
|
||||
gray2rgb,
|
||||
gray2rgba,
|
||||
xyz2lab,
|
||||
lab2xyz,
|
||||
lab2rgb,
|
||||
rgb2lab,
|
||||
xyz2luv,
|
||||
luv2xyz,
|
||||
luv2rgb,
|
||||
rgb2luv,
|
||||
rgb2hed,
|
||||
hed2rgb,
|
||||
lab2lch,
|
||||
lch2lab,
|
||||
rgb2yuv,
|
||||
yuv2rgb,
|
||||
rgb2yiq,
|
||||
yiq2rgb,
|
||||
rgb2ypbpr,
|
||||
ypbpr2rgb,
|
||||
rgb2ycbcr,
|
||||
ycbcr2rgb,
|
||||
rgb2ydbdr,
|
||||
ydbdr2rgb,
|
||||
separate_stains,
|
||||
combine_stains,
|
||||
rgb_from_hed,
|
||||
hed_from_rgb,
|
||||
rgb_from_hdx,
|
||||
hdx_from_rgb,
|
||||
rgb_from_fgx,
|
||||
fgx_from_rgb,
|
||||
rgb_from_bex,
|
||||
bex_from_rgb,
|
||||
rgb_from_rbd,
|
||||
rbd_from_rgb,
|
||||
rgb_from_gdx,
|
||||
gdx_from_rgb,
|
||||
rgb_from_hax,
|
||||
hax_from_rgb,
|
||||
rgb_from_bro,
|
||||
bro_from_rgb,
|
||||
rgb_from_bpx,
|
||||
bpx_from_rgb,
|
||||
rgb_from_ahx,
|
||||
ahx_from_rgb,
|
||||
rgb_from_hpx,
|
||||
hpx_from_rgb,
|
||||
)
|
||||
|
||||
from .colorlabel import color_dict, label2rgb
|
||||
from .delta_e import (
|
||||
deltaE_cie76,
|
||||
deltaE_ciede94,
|
||||
deltaE_ciede2000,
|
||||
deltaE_cmc,
|
||||
)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,81 @@
|
||||
import functools
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .. import color
|
||||
from ..util.dtype import _convert
|
||||
|
||||
|
||||
__all__ = ['adapt_rgb', 'hsv_value', 'each_channel']
|
||||
|
||||
|
||||
def is_rgb_like(image, channel_axis=-1):
|
||||
"""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[channel_axis] 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(image_filter)
|
||||
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.
|
||||
"""
|
||||
# Slice the first three channels so that we remove any alpha channels.
|
||||
hsv = color.rgb2hsv(image[:, :, :3])
|
||||
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 np.moveaxis(image, -1, 0)]
|
||||
return np.stack(c_new, axis=-1)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,299 @@
|
||||
import itertools
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .._shared.utils import _supported_float_type, warn
|
||||
from ..util import img_as_float
|
||||
from . import rgb_colors
|
||||
from .colorconv import gray2rgb, rgb2hsv, hsv2rgb
|
||||
|
||||
|
||||
__all__ = ['color_dict', 'label2rgb', 'DEFAULT_COLORS']
|
||||
|
||||
|
||||
DEFAULT_COLORS = (
|
||||
'red',
|
||||
'blue',
|
||||
'yellow',
|
||||
'magenta',
|
||||
'green',
|
||||
'indigo',
|
||||
'darkorange',
|
||||
'cyan',
|
||||
'pink',
|
||||
'yellowgreen',
|
||||
)
|
||||
|
||||
|
||||
color_dict = {k: v for k, v in rgb_colors.__dict__.items() if isinstance(v, tuple)}
|
||||
|
||||
|
||||
def _rgb_vector(color):
|
||||
"""Return RGB color as (1, 3) array.
|
||||
|
||||
This RGB array gets multiplied by masked regions of an RGB image, which are
|
||||
partially flattened by masking (i.e. dimensions 2D + RGB -> 1D + RGB).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
color : str or array
|
||||
Color name in ``skimage.color.color_dict`` or RGB float values between [0, 1].
|
||||
"""
|
||||
if isinstance(color, str):
|
||||
color = color_dict[color]
|
||||
# Slice to handle RGBA colors.
|
||||
return np.array(color[:3])
|
||||
|
||||
|
||||
def _match_label_with_color(label, colors, bg_label, bg_color):
|
||||
"""Return `unique_labels` and `color_cycle` for label array and color list.
|
||||
|
||||
Colors are cycled for normal labels, but the background color should only
|
||||
be used for the background.
|
||||
"""
|
||||
# Temporarily set background color; it will be removed later.
|
||||
if bg_color is None:
|
||||
bg_color = (0, 0, 0)
|
||||
bg_color = _rgb_vector(bg_color)
|
||||
|
||||
# map labels to their ranks among all labels from small to large
|
||||
unique_labels, mapped_labels = np.unique(label, return_inverse=True)
|
||||
# unique_inverse is no longer flat in NumPy 2.0
|
||||
mapped_labels = mapped_labels.reshape(-1)
|
||||
|
||||
# get rank of bg_label
|
||||
bg_label_rank_list = mapped_labels[label.flat == bg_label]
|
||||
|
||||
# The rank of each label is the index of the color it is matched to in
|
||||
# color cycle. bg_label should always be mapped to the first color, so
|
||||
# its rank must be 0. Other labels should be ranked from small to large
|
||||
# from 1.
|
||||
if len(bg_label_rank_list) > 0:
|
||||
bg_label_rank = bg_label_rank_list[0]
|
||||
mapped_labels[mapped_labels < bg_label_rank] += 1
|
||||
mapped_labels[label.flat == bg_label] = 0
|
||||
else:
|
||||
mapped_labels += 1
|
||||
|
||||
# Modify labels and color cycle so background color is used only once.
|
||||
color_cycle = itertools.cycle(colors)
|
||||
color_cycle = itertools.chain([bg_color], color_cycle)
|
||||
|
||||
return mapped_labels, color_cycle
|
||||
|
||||
|
||||
def label2rgb(
|
||||
label,
|
||||
image=None,
|
||||
colors=None,
|
||||
alpha=0.3,
|
||||
bg_label=0,
|
||||
bg_color=(0, 0, 0),
|
||||
image_alpha=1,
|
||||
kind='overlay',
|
||||
*,
|
||||
saturation=0,
|
||||
channel_axis=-1,
|
||||
):
|
||||
"""Return an RGB image where color-coded labels are painted over the image.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : ndarray
|
||||
Integer array of labels with the same shape as `image`.
|
||||
image : ndarray, optional
|
||||
Image used as underlay for labels. It should have the same shape as
|
||||
`labels`, optionally with an additional RGB (channels) axis. If `image`
|
||||
is an RGB image, it is 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. If `bg_label` is specified,
|
||||
`bg_color` is `None`, and `kind` is `overlay`,
|
||||
background is not painted by any colors.
|
||||
bg_color : str or array, optional
|
||||
Background color. Must be a name in ``skimage.color.color_dict`` or RGB float
|
||||
values 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.
|
||||
saturation : float [0, 1], optional
|
||||
Parameter to control the saturation applied to the original image
|
||||
between fully saturated (original RGB, `saturation=1`) and fully
|
||||
unsaturated (grayscale, `saturation=0`). Only applies when
|
||||
`kind='overlay'`.
|
||||
channel_axis : int, optional
|
||||
This parameter indicates which axis of the output array will correspond
|
||||
to channels. If `image` is provided, this must also match the axis of
|
||||
`image` that corresponds to channels.
|
||||
|
||||
.. versionadded:: 0.19
|
||||
``channel_axis`` was added in 0.19.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : ndarray of float, same shape as `image`
|
||||
The result of blending a cycling colormap (`colors`) for each distinct
|
||||
value in `label` with the image, at a certain alpha value.
|
||||
"""
|
||||
if image is not None:
|
||||
image = np.moveaxis(image, source=channel_axis, destination=-1)
|
||||
if kind == 'overlay':
|
||||
rgb = _label2rgb_overlay(
|
||||
label, image, colors, alpha, bg_label, bg_color, image_alpha, saturation
|
||||
)
|
||||
elif kind == 'avg':
|
||||
rgb = _label2rgb_avg(label, image, bg_label, bg_color)
|
||||
else:
|
||||
raise ValueError("`kind` must be either 'overlay' or 'avg'.")
|
||||
return np.moveaxis(rgb, source=-1, destination=channel_axis)
|
||||
|
||||
|
||||
def _label2rgb_overlay(
|
||||
label,
|
||||
image=None,
|
||||
colors=None,
|
||||
alpha=0.3,
|
||||
bg_label=-1,
|
||||
bg_color=None,
|
||||
image_alpha=1,
|
||||
saturation=0,
|
||||
):
|
||||
"""Return an RGB image where color-coded labels are painted over the image.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : ndarray
|
||||
Integer array of labels with the same shape as `image`.
|
||||
image : ndarray, optional
|
||||
Image used as underlay for labels. It should have the same shape as
|
||||
`labels`, optionally with an additional RGB (channels) axis. If `image`
|
||||
is an RGB image, it is 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. If `bg_label` is specified and
|
||||
`bg_color` is `None`, background is not painted by any colors.
|
||||
bg_color : str or array, optional
|
||||
Background color. Must be a name in ``skimage.color.color_dict`` or RGB float
|
||||
values between [0, 1].
|
||||
image_alpha : float [0, 1], optional
|
||||
Opacity of the image.
|
||||
saturation : float [0, 1], optional
|
||||
Parameter to control the saturation applied to the original image
|
||||
between fully saturated (original RGB, `saturation=1`) and fully
|
||||
unsaturated (grayscale, `saturation=0`).
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : ndarray of float, same shape as `image`
|
||||
The result of blending a cycling colormap (`colors`) for each distinct
|
||||
value in `label` with the image, at a certain alpha value.
|
||||
"""
|
||||
if not 0 <= saturation <= 1:
|
||||
warn(f'saturation must be in range [0, 1], got {saturation}')
|
||||
|
||||
if colors is None:
|
||||
colors = DEFAULT_COLORS
|
||||
colors = [_rgb_vector(c) for c in colors]
|
||||
|
||||
if image is None:
|
||||
image = np.zeros(label.shape + (3,), dtype=np.float64)
|
||||
# Opacity doesn't make sense if no image exists.
|
||||
alpha = 1
|
||||
else:
|
||||
if image.shape[: label.ndim] != label.shape or image.ndim > label.ndim + 1:
|
||||
raise ValueError("`image` and `label` must be the same shape")
|
||||
|
||||
if image.ndim == label.ndim + 1 and image.shape[-1] != 3:
|
||||
raise ValueError("`image` must be RGB (image.shape[-1] must be 3).")
|
||||
|
||||
if image.min() < 0:
|
||||
warn("Negative intensities in `image` are not supported")
|
||||
|
||||
float_dtype = _supported_float_type(image.dtype)
|
||||
image = img_as_float(image).astype(float_dtype, copy=False)
|
||||
if image.ndim > label.ndim:
|
||||
hsv = rgb2hsv(image)
|
||||
hsv[..., 1] *= saturation
|
||||
image = hsv2rgb(hsv)
|
||||
elif image.ndim == label.ndim:
|
||||
image = gray2rgb(image)
|
||||
image = image * image_alpha + (1 - image_alpha)
|
||||
|
||||
# Ensure that all labels are non-negative so we can index into
|
||||
# `label_to_color` correctly.
|
||||
offset = min(label.min(), bg_label)
|
||||
if offset != 0:
|
||||
label = label - offset # Make sure you don't modify the input array.
|
||||
bg_label -= offset
|
||||
|
||||
new_type = np.min_scalar_type(int(label.max()))
|
||||
if new_type == bool:
|
||||
new_type = np.uint8
|
||||
label = label.astype(new_type)
|
||||
|
||||
mapped_labels_flat, color_cycle = _match_label_with_color(
|
||||
label, colors, bg_label, bg_color
|
||||
)
|
||||
|
||||
if len(mapped_labels_flat) == 0:
|
||||
return image
|
||||
|
||||
dense_labels = range(np.max(mapped_labels_flat) + 1)
|
||||
|
||||
label_to_color = np.stack([c for i, c in zip(dense_labels, color_cycle)])
|
||||
|
||||
mapped_labels = label
|
||||
mapped_labels.flat = mapped_labels_flat
|
||||
result = label_to_color[mapped_labels] * alpha + image * (1 - alpha)
|
||||
|
||||
# Remove background label if its color was not specified.
|
||||
remove_background = 0 in mapped_labels_flat and bg_color is None
|
||||
if remove_background:
|
||||
result[label == bg_label] = image[label == bg_label]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
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
|
||||
----------
|
||||
label_field : ndarray 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 : ndarray, same shape and type as `image`
|
||||
The output visualization.
|
||||
"""
|
||||
out = np.zeros(label_field.shape + (3,), dtype=image.dtype)
|
||||
labels = np.unique(label_field)
|
||||
bg = labels == bg_label
|
||||
if bg.any():
|
||||
labels = labels[labels != bg_label]
|
||||
mask = (label_field == bg_label).nonzero()
|
||||
out[mask] = bg_color
|
||||
for label in labels:
|
||||
mask = (label_field == label).nonzero()
|
||||
color = image[mask].mean(axis=0)
|
||||
out[mask] = color
|
||||
return out
|
||||
@@ -0,0 +1,393 @@
|
||||
"""
|
||||
Functions for calculating the "distance" between colors.
|
||||
|
||||
Implicit in these definitions of "distance" is the notion of "Just Noticeable
|
||||
Distance" (JND). This represents the distance between colors where a human can
|
||||
perceive different colors. Humans are more sensitive to certain colors than
|
||||
others, which different deltaE metrics correct for with varying degrees of
|
||||
sophistication.
|
||||
|
||||
The literature often mentions 1 as the minimum distance for visual
|
||||
differentiation, but more recent studies (Mahy 1994) peg JND at 2.3
|
||||
|
||||
The delta-E notation comes from the German word for "Sensation" (Empfindung).
|
||||
|
||||
Reference
|
||||
---------
|
||||
https://en.wikipedia.org/wiki/Color_difference
|
||||
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .._shared.utils import _supported_float_type
|
||||
from .colorconv import lab2lch, _cart2polar_2pi
|
||||
|
||||
|
||||
def _float_inputs(lab1, lab2, allow_float32=True):
|
||||
lab1 = np.asarray(lab1)
|
||||
lab2 = np.asarray(lab2)
|
||||
if allow_float32:
|
||||
float_dtype = _supported_float_type((lab1.dtype, lab2.dtype))
|
||||
else:
|
||||
float_dtype = np.float64
|
||||
lab1 = lab1.astype(float_dtype, copy=False)
|
||||
lab2 = lab2.astype(float_dtype, copy=False)
|
||||
return lab1, lab2
|
||||
|
||||
|
||||
def deltaE_cie76(lab1, lab2, channel_axis=-1):
|
||||
"""Euclidean distance between two points in Lab color space
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lab1 : array_like
|
||||
reference color (Lab colorspace)
|
||||
lab2 : array_like
|
||||
comparison color (Lab colorspace)
|
||||
channel_axis : int, optional
|
||||
This parameter indicates which axis of the arrays corresponds to
|
||||
channels.
|
||||
|
||||
.. versionadded:: 0.19
|
||||
``channel_axis`` was added in 0.19.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dE : array_like
|
||||
distance between colors `lab1` and `lab2`
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] https://en.wikipedia.org/wiki/Color_difference
|
||||
.. [2] A. R. Robertson, "The CIE 1976 color-difference formulae,"
|
||||
Color Res. Appl. 2, 7-11 (1977).
|
||||
"""
|
||||
lab1, lab2 = _float_inputs(lab1, lab2, allow_float32=True)
|
||||
L1, a1, b1 = np.moveaxis(lab1, source=channel_axis, destination=0)[:3]
|
||||
L2, a2, b2 = np.moveaxis(lab2, source=channel_axis, destination=0)[:3]
|
||||
return np.sqrt((L2 - L1) ** 2 + (a2 - a1) ** 2 + (b2 - b1) ** 2)
|
||||
|
||||
|
||||
def deltaE_ciede94(
|
||||
lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015, *, channel_axis=-1
|
||||
):
|
||||
"""Color difference according to CIEDE 94 standard
|
||||
|
||||
Accommodates perceptual non-uniformities through the use of application
|
||||
specific scale factors (`kH`, `kC`, `kL`, `k1`, and `k2`).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lab1 : array_like
|
||||
reference color (Lab colorspace)
|
||||
lab2 : array_like
|
||||
comparison color (Lab colorspace)
|
||||
kH : float, optional
|
||||
Hue scale
|
||||
kC : float, optional
|
||||
Chroma scale
|
||||
kL : float, optional
|
||||
Lightness scale
|
||||
k1 : float, optional
|
||||
first scale parameter
|
||||
k2 : float, optional
|
||||
second scale parameter
|
||||
channel_axis : int, optional
|
||||
This parameter indicates which axis of the arrays corresponds to
|
||||
channels.
|
||||
|
||||
.. versionadded:: 0.19
|
||||
``channel_axis`` was added in 0.19.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dE : array_like
|
||||
color difference between `lab1` and `lab2`
|
||||
|
||||
Notes
|
||||
-----
|
||||
deltaE_ciede94 is not symmetric with respect to lab1 and lab2. CIEDE94
|
||||
defines the scales for the lightness, hue, and chroma in terms of the first
|
||||
color. Consequently, the first color should be regarded as the "reference"
|
||||
color.
|
||||
|
||||
`kL`, `k1`, `k2` depend on the application and default to the values
|
||||
suggested for graphic arts
|
||||
|
||||
========== ============== ==========
|
||||
Parameter Graphic Arts Textiles
|
||||
========== ============== ==========
|
||||
`kL` 1.000 2.000
|
||||
`k1` 0.045 0.048
|
||||
`k2` 0.015 0.014
|
||||
========== ============== ==========
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] https://en.wikipedia.org/wiki/Color_difference
|
||||
.. [2] http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html
|
||||
"""
|
||||
lab1, lab2 = _float_inputs(lab1, lab2, allow_float32=True)
|
||||
lab1 = np.moveaxis(lab1, source=channel_axis, destination=0)
|
||||
lab2 = np.moveaxis(lab2, source=channel_axis, destination=0)
|
||||
|
||||
L1, C1 = lab2lch(lab1, channel_axis=0)[:2]
|
||||
L2, C2 = lab2lch(lab2, channel_axis=0)[:2]
|
||||
|
||||
dL = L1 - L2
|
||||
dC = C1 - C2
|
||||
dH2 = get_dH2(lab1, lab2, channel_axis=0)
|
||||
|
||||
SL = 1
|
||||
SC = 1 + k1 * C1
|
||||
SH = 1 + k2 * C1
|
||||
|
||||
dE2 = (dL / (kL * SL)) ** 2
|
||||
dE2 += (dC / (kC * SC)) ** 2
|
||||
dE2 += dH2 / (kH * SH) ** 2
|
||||
return np.sqrt(np.maximum(dE2, 0))
|
||||
|
||||
|
||||
def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1, *, channel_axis=-1):
|
||||
"""Color difference as given by the CIEDE 2000 standard.
|
||||
|
||||
CIEDE 2000 is a major revision of CIDE94. The perceptual calibration is
|
||||
largely based on experience with automotive paint on smooth surfaces.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lab1 : array_like
|
||||
reference color (Lab colorspace)
|
||||
lab2 : array_like
|
||||
comparison color (Lab colorspace)
|
||||
kL : float (range), optional
|
||||
lightness scale factor, 1 for "acceptably close"; 2 for "imperceptible"
|
||||
see deltaE_cmc
|
||||
kC : float (range), optional
|
||||
chroma scale factor, usually 1
|
||||
kH : float (range), optional
|
||||
hue scale factor, usually 1
|
||||
channel_axis : int, optional
|
||||
This parameter indicates which axis of the arrays corresponds to
|
||||
channels.
|
||||
|
||||
.. versionadded:: 0.19
|
||||
``channel_axis`` was added in 0.19.
|
||||
|
||||
Returns
|
||||
-------
|
||||
deltaE : array_like
|
||||
The distance between `lab1` and `lab2`
|
||||
|
||||
Notes
|
||||
-----
|
||||
CIEDE 2000 assumes parametric weighting factors for the lightness, chroma,
|
||||
and hue (`kL`, `kC`, `kH` respectively). These default to 1.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] https://en.wikipedia.org/wiki/Color_difference
|
||||
.. [2] http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf
|
||||
:DOI:`10.1364/AO.33.008069`
|
||||
.. [3] M. Melgosa, J. Quesada, and E. Hita, "Uniformity of some recent
|
||||
color metrics tested with an accurate color-difference tolerance
|
||||
dataset," Appl. Opt. 33, 8069-8077 (1994).
|
||||
"""
|
||||
lab1, lab2 = _float_inputs(lab1, lab2, allow_float32=True)
|
||||
|
||||
channel_axis = channel_axis % lab1.ndim
|
||||
unroll = False
|
||||
if lab1.ndim == 1 and lab2.ndim == 1:
|
||||
unroll = True
|
||||
if lab1.ndim == 1:
|
||||
lab1 = lab1[None, :]
|
||||
if lab2.ndim == 1:
|
||||
lab2 = lab2[None, :]
|
||||
channel_axis += 1
|
||||
L1, a1, b1 = np.moveaxis(lab1, source=channel_axis, destination=0)[:3]
|
||||
L2, a2, b2 = np.moveaxis(lab2, source=channel_axis, destination=0)[:3]
|
||||
|
||||
# distort `a` based on average chroma
|
||||
# then convert to lch coordinates from distorted `a`
|
||||
# all subsequence calculations are in the new coordinates
|
||||
# (often denoted "prime" in the literature)
|
||||
Cbar = 0.5 * (np.hypot(a1, b1) + np.hypot(a2, b2))
|
||||
c7 = Cbar**7
|
||||
G = 0.5 * (1 - np.sqrt(c7 / (c7 + 25**7)))
|
||||
scale = 1 + G
|
||||
C1, h1 = _cart2polar_2pi(a1 * scale, b1)
|
||||
C2, h2 = _cart2polar_2pi(a2 * scale, b2)
|
||||
# recall that c, h are polar coordinates. c==r, h==theta
|
||||
|
||||
# cide2000 has four terms to delta_e:
|
||||
# 1) Luminance term
|
||||
# 2) Hue term
|
||||
# 3) Chroma term
|
||||
# 4) hue Rotation term
|
||||
|
||||
# lightness term
|
||||
Lbar = 0.5 * (L1 + L2)
|
||||
tmp = (Lbar - 50) ** 2
|
||||
SL = 1 + 0.015 * tmp / np.sqrt(20 + tmp)
|
||||
L_term = (L2 - L1) / (kL * SL)
|
||||
|
||||
# chroma term
|
||||
Cbar = 0.5 * (C1 + C2) # new coordinates
|
||||
SC = 1 + 0.045 * Cbar
|
||||
C_term = (C2 - C1) / (kC * SC)
|
||||
|
||||
# hue term
|
||||
h_diff = h2 - h1
|
||||
h_sum = h1 + h2
|
||||
CC = C1 * C2
|
||||
|
||||
dH = h_diff.copy()
|
||||
dH[h_diff > np.pi] -= 2 * np.pi
|
||||
dH[h_diff < -np.pi] += 2 * np.pi
|
||||
dH[CC == 0.0] = 0.0 # if r == 0, dtheta == 0
|
||||
dH_term = 2 * np.sqrt(CC) * np.sin(dH / 2)
|
||||
|
||||
Hbar = h_sum.copy()
|
||||
mask = np.logical_and(CC != 0.0, np.abs(h_diff) > np.pi)
|
||||
Hbar[mask * (h_sum < 2 * np.pi)] += 2 * np.pi
|
||||
Hbar[mask * (h_sum >= 2 * np.pi)] -= 2 * np.pi
|
||||
Hbar[CC == 0.0] *= 2
|
||||
Hbar *= 0.5
|
||||
|
||||
T = (
|
||||
1
|
||||
- 0.17 * np.cos(Hbar - np.deg2rad(30))
|
||||
+ 0.24 * np.cos(2 * Hbar)
|
||||
+ 0.32 * np.cos(3 * Hbar + np.deg2rad(6))
|
||||
- 0.20 * np.cos(4 * Hbar - np.deg2rad(63))
|
||||
)
|
||||
SH = 1 + 0.015 * Cbar * T
|
||||
|
||||
H_term = dH_term / (kH * SH)
|
||||
|
||||
# hue rotation
|
||||
c7 = Cbar**7
|
||||
Rc = 2 * np.sqrt(c7 / (c7 + 25**7))
|
||||
dtheta = np.deg2rad(30) * np.exp(-(((np.rad2deg(Hbar) - 275) / 25) ** 2))
|
||||
R_term = -np.sin(2 * dtheta) * Rc * C_term * H_term
|
||||
|
||||
# put it all together
|
||||
dE2 = L_term**2
|
||||
dE2 += C_term**2
|
||||
dE2 += H_term**2
|
||||
dE2 += R_term
|
||||
ans = np.sqrt(np.maximum(dE2, 0))
|
||||
if unroll:
|
||||
ans = ans[0]
|
||||
return ans
|
||||
|
||||
|
||||
def deltaE_cmc(lab1, lab2, kL=1, kC=1, *, channel_axis=-1):
|
||||
"""Color difference from the CMC l:c standard.
|
||||
|
||||
This color difference was developed by the Colour Measurement Committee
|
||||
(CMC) of the Society of Dyers and Colourists (United Kingdom). It is
|
||||
intended for use in the textile industry.
|
||||
|
||||
The scale factors `kL`, `kC` set the weight given to differences in
|
||||
lightness and chroma relative to differences in hue. The usual values are
|
||||
``kL=2``, ``kC=1`` for "acceptability" and ``kL=1``, ``kC=1`` for
|
||||
"imperceptibility". Colors with ``dE > 1`` are "different" for the given
|
||||
scale factors.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lab1 : array_like
|
||||
reference color (Lab colorspace)
|
||||
lab2 : array_like
|
||||
comparison color (Lab colorspace)
|
||||
channel_axis : int, optional
|
||||
This parameter indicates which axis of the arrays corresponds to
|
||||
channels.
|
||||
|
||||
.. versionadded:: 0.19
|
||||
``channel_axis`` was added in 0.19.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dE : array_like
|
||||
distance between colors `lab1` and `lab2`
|
||||
|
||||
Notes
|
||||
-----
|
||||
deltaE_cmc the defines the scales for the lightness, hue, and chroma
|
||||
in terms of the first color. Consequently
|
||||
``deltaE_cmc(lab1, lab2) != deltaE_cmc(lab2, lab1)``
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] https://en.wikipedia.org/wiki/Color_difference
|
||||
.. [2] http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html
|
||||
.. [3] F. J. J. Clarke, R. McDonald, and B. Rigg, "Modification to the
|
||||
JPC79 colour-difference formula," J. Soc. Dyers Colour. 100, 128-132
|
||||
(1984).
|
||||
"""
|
||||
lab1, lab2 = _float_inputs(lab1, lab2, allow_float32=True)
|
||||
lab1 = np.moveaxis(lab1, source=channel_axis, destination=0)
|
||||
lab2 = np.moveaxis(lab2, source=channel_axis, destination=0)
|
||||
L1, C1, h1 = lab2lch(lab1, channel_axis=0)[:3]
|
||||
L2, C2, h2 = lab2lch(lab2, channel_axis=0)[:3]
|
||||
|
||||
dC = C1 - C2
|
||||
dL = L1 - L2
|
||||
dH2 = get_dH2(lab1, lab2, channel_axis=0)
|
||||
|
||||
T = np.where(
|
||||
np.logical_and(np.rad2deg(h1) >= 164, np.rad2deg(h1) <= 345),
|
||||
0.56 + 0.2 * np.abs(np.cos(h1 + np.deg2rad(168))),
|
||||
0.36 + 0.4 * np.abs(np.cos(h1 + np.deg2rad(35))),
|
||||
)
|
||||
c1_4 = C1**4
|
||||
F = np.sqrt(c1_4 / (c1_4 + 1900))
|
||||
|
||||
SL = np.where(L1 < 16, 0.511, 0.040975 * L1 / (1.0 + 0.01765 * L1))
|
||||
SC = 0.638 + 0.0638 * C1 / (1.0 + 0.0131 * C1)
|
||||
SH = SC * (F * T + 1 - F)
|
||||
|
||||
dE2 = (dL / (kL * SL)) ** 2
|
||||
dE2 += (dC / (kC * SC)) ** 2
|
||||
dE2 += dH2 / (SH**2)
|
||||
|
||||
return np.sqrt(np.maximum(dE2, 0))
|
||||
|
||||
|
||||
def get_dH2(lab1, lab2, *, channel_axis=-1):
|
||||
"""squared hue difference term occurring in deltaE_cmc and deltaE_ciede94
|
||||
|
||||
Despite its name, "dH" is not a simple difference of hue values. We avoid
|
||||
working directly with the hue value, since differencing angles is
|
||||
troublesome. The hue term is usually written as:
|
||||
c1 = sqrt(a1**2 + b1**2)
|
||||
c2 = sqrt(a2**2 + b2**2)
|
||||
term = (a1-a2)**2 + (b1-b2)**2 - (c1-c2)**2
|
||||
dH = sqrt(term)
|
||||
|
||||
However, this has poor roundoff properties when a or b is dominant.
|
||||
Instead, ab is a vector with elements a and b. The same dH term can be
|
||||
re-written as:
|
||||
|ab1-ab2|**2 - (|ab1| - |ab2|)**2
|
||||
and then simplified to:
|
||||
2*|ab1|*|ab2| - 2*dot(ab1, ab2)
|
||||
"""
|
||||
# This function needs double precision internally for accuracy
|
||||
input_is_float_32 = _supported_float_type((lab1.dtype, lab2.dtype)) == np.float32
|
||||
lab1, lab2 = _float_inputs(lab1, lab2, allow_float32=False)
|
||||
|
||||
a1, b1 = np.moveaxis(lab1, source=channel_axis, destination=0)[1:3]
|
||||
a2, b2 = np.moveaxis(lab2, source=channel_axis, destination=0)[1:3]
|
||||
|
||||
# magnitude of (a, b) is the chroma
|
||||
C1 = np.hypot(a1, b1)
|
||||
C2 = np.hypot(a2, b2)
|
||||
|
||||
term = (C1 * C2) - (a1 * a2 + b1 * b2)
|
||||
out = 2 * term
|
||||
if input_is_float_32:
|
||||
out = out.astype(np.float32)
|
||||
return out
|
||||
@@ -0,0 +1,146 @@
|
||||
aliceblue = (0.941, 0.973, 1)
|
||||
antiquewhite = (0.98, 0.922, 0.843)
|
||||
aqua = (0, 1, 1)
|
||||
aquamarine = (0.498, 1, 0.831)
|
||||
azure = (0.941, 1, 1)
|
||||
beige = (0.961, 0.961, 0.863)
|
||||
bisque = (1, 0.894, 0.769)
|
||||
black = (0, 0, 0)
|
||||
blanchedalmond = (1, 0.922, 0.804)
|
||||
blue = (0, 0, 1)
|
||||
blueviolet = (0.541, 0.169, 0.886)
|
||||
brown = (0.647, 0.165, 0.165)
|
||||
burlywood = (0.871, 0.722, 0.529)
|
||||
cadetblue = (0.373, 0.62, 0.627)
|
||||
chartreuse = (0.498, 1, 0)
|
||||
chocolate = (0.824, 0.412, 0.118)
|
||||
coral = (1, 0.498, 0.314)
|
||||
cornflowerblue = (0.392, 0.584, 0.929)
|
||||
cornsilk = (1, 0.973, 0.863)
|
||||
crimson = (0.863, 0.0784, 0.235)
|
||||
cyan = (0, 1, 1)
|
||||
darkblue = (0, 0, 0.545)
|
||||
darkcyan = (0, 0.545, 0.545)
|
||||
darkgoldenrod = (0.722, 0.525, 0.0431)
|
||||
darkgray = (0.663, 0.663, 0.663)
|
||||
darkgreen = (0, 0.392, 0)
|
||||
darkgrey = (0.663, 0.663, 0.663)
|
||||
darkkhaki = (0.741, 0.718, 0.42)
|
||||
darkmagenta = (0.545, 0, 0.545)
|
||||
darkolivegreen = (0.333, 0.42, 0.184)
|
||||
darkorange = (1, 0.549, 0)
|
||||
darkorchid = (0.6, 0.196, 0.8)
|
||||
darkred = (0.545, 0, 0)
|
||||
darksalmon = (0.914, 0.588, 0.478)
|
||||
darkseagreen = (0.561, 0.737, 0.561)
|
||||
darkslateblue = (0.282, 0.239, 0.545)
|
||||
darkslategray = (0.184, 0.31, 0.31)
|
||||
darkslategrey = (0.184, 0.31, 0.31)
|
||||
darkturquoise = (0, 0.808, 0.82)
|
||||
darkviolet = (0.58, 0, 0.827)
|
||||
deeppink = (1, 0.0784, 0.576)
|
||||
deepskyblue = (0, 0.749, 1)
|
||||
dimgray = (0.412, 0.412, 0.412)
|
||||
dimgrey = (0.412, 0.412, 0.412)
|
||||
dodgerblue = (0.118, 0.565, 1)
|
||||
firebrick = (0.698, 0.133, 0.133)
|
||||
floralwhite = (1, 0.98, 0.941)
|
||||
forestgreen = (0.133, 0.545, 0.133)
|
||||
fuchsia = (1, 0, 1)
|
||||
gainsboro = (0.863, 0.863, 0.863)
|
||||
ghostwhite = (0.973, 0.973, 1)
|
||||
gold = (1, 0.843, 0)
|
||||
goldenrod = (0.855, 0.647, 0.125)
|
||||
gray = (0.502, 0.502, 0.502)
|
||||
green = (0, 0.502, 0)
|
||||
greenyellow = (0.678, 1, 0.184)
|
||||
grey = (0.502, 0.502, 0.502)
|
||||
honeydew = (0.941, 1, 0.941)
|
||||
hotpink = (1, 0.412, 0.706)
|
||||
indianred = (0.804, 0.361, 0.361)
|
||||
indigo = (0.294, 0, 0.51)
|
||||
ivory = (1, 1, 0.941)
|
||||
khaki = (0.941, 0.902, 0.549)
|
||||
lavender = (0.902, 0.902, 0.98)
|
||||
lavenderblush = (1, 0.941, 0.961)
|
||||
lawngreen = (0.486, 0.988, 0)
|
||||
lemonchiffon = (1, 0.98, 0.804)
|
||||
lightblue = (0.678, 0.847, 0.902)
|
||||
lightcoral = (0.941, 0.502, 0.502)
|
||||
lightcyan = (0.878, 1, 1)
|
||||
lightgoldenrodyellow = (0.98, 0.98, 0.824)
|
||||
lightgray = (0.827, 0.827, 0.827)
|
||||
lightgreen = (0.565, 0.933, 0.565)
|
||||
lightgrey = (0.827, 0.827, 0.827)
|
||||
lightpink = (1, 0.714, 0.757)
|
||||
lightsalmon = (1, 0.627, 0.478)
|
||||
lightseagreen = (0.125, 0.698, 0.667)
|
||||
lightskyblue = (0.529, 0.808, 0.98)
|
||||
lightslategray = (0.467, 0.533, 0.6)
|
||||
lightslategrey = (0.467, 0.533, 0.6)
|
||||
lightsteelblue = (0.69, 0.769, 0.871)
|
||||
lightyellow = (1, 1, 0.878)
|
||||
lime = (0, 1, 0)
|
||||
limegreen = (0.196, 0.804, 0.196)
|
||||
linen = (0.98, 0.941, 0.902)
|
||||
magenta = (1, 0, 1)
|
||||
maroon = (0.502, 0, 0)
|
||||
mediumaquamarine = (0.4, 0.804, 0.667)
|
||||
mediumblue = (0, 0, 0.804)
|
||||
mediumorchid = (0.729, 0.333, 0.827)
|
||||
mediumpurple = (0.576, 0.439, 0.859)
|
||||
mediumseagreen = (0.235, 0.702, 0.443)
|
||||
mediumslateblue = (0.482, 0.408, 0.933)
|
||||
mediumspringgreen = (0, 0.98, 0.604)
|
||||
mediumturquoise = (0.282, 0.82, 0.8)
|
||||
mediumvioletred = (0.78, 0.0824, 0.522)
|
||||
midnightblue = (0.098, 0.098, 0.439)
|
||||
mintcream = (0.961, 1, 0.98)
|
||||
mistyrose = (1, 0.894, 0.882)
|
||||
moccasin = (1, 0.894, 0.71)
|
||||
navajowhite = (1, 0.871, 0.678)
|
||||
navy = (0, 0, 0.502)
|
||||
oldlace = (0.992, 0.961, 0.902)
|
||||
olive = (0.502, 0.502, 0)
|
||||
olivedrab = (0.42, 0.557, 0.137)
|
||||
orange = (1, 0.647, 0)
|
||||
orangered = (1, 0.271, 0)
|
||||
orchid = (0.855, 0.439, 0.839)
|
||||
palegoldenrod = (0.933, 0.91, 0.667)
|
||||
palegreen = (0.596, 0.984, 0.596)
|
||||
palevioletred = (0.686, 0.933, 0.933)
|
||||
papayawhip = (1, 0.937, 0.835)
|
||||
peachpuff = (1, 0.855, 0.725)
|
||||
peru = (0.804, 0.522, 0.247)
|
||||
pink = (1, 0.753, 0.796)
|
||||
plum = (0.867, 0.627, 0.867)
|
||||
powderblue = (0.69, 0.878, 0.902)
|
||||
purple = (0.502, 0, 0.502)
|
||||
red = (1, 0, 0)
|
||||
rosybrown = (0.737, 0.561, 0.561)
|
||||
royalblue = (0.255, 0.412, 0.882)
|
||||
saddlebrown = (0.545, 0.271, 0.0745)
|
||||
salmon = (0.98, 0.502, 0.447)
|
||||
sandybrown = (0.98, 0.643, 0.376)
|
||||
seagreen = (0.18, 0.545, 0.341)
|
||||
seashell = (1, 0.961, 0.933)
|
||||
sienna = (0.627, 0.322, 0.176)
|
||||
silver = (0.753, 0.753, 0.753)
|
||||
skyblue = (0.529, 0.808, 0.922)
|
||||
slateblue = (0.416, 0.353, 0.804)
|
||||
slategray = (0.439, 0.502, 0.565)
|
||||
slategrey = (0.439, 0.502, 0.565)
|
||||
snow = (1, 0.98, 0.98)
|
||||
springgreen = (0, 1, 0.498)
|
||||
steelblue = (0.275, 0.51, 0.706)
|
||||
tan = (0.824, 0.706, 0.549)
|
||||
teal = (0, 0.502, 0.502)
|
||||
thistle = (0.847, 0.749, 0.847)
|
||||
tomato = (1, 0.388, 0.278)
|
||||
turquoise = (0.251, 0.878, 0.816)
|
||||
violet = (0.933, 0.51, 0.933)
|
||||
wheat = (0.961, 0.871, 0.702)
|
||||
white = (1, 1, 1)
|
||||
whitesmoke = (0.961, 0.961, 0.961)
|
||||
yellow = (1, 1, 0)
|
||||
yellowgreen = (0.604, 0.804, 0.196)
|
||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,94 @@
|
||||
from functools import partial
|
||||
|
||||
import numpy as np
|
||||
|
||||
from skimage import img_as_float, img_as_uint
|
||||
from skimage import color, data, filters
|
||||
from skimage.color.adapt_rgb import adapt_rgb, each_channel, hsv_value
|
||||
|
||||
# Down-sample image for quicker testing.
|
||||
COLOR_IMAGE = data.astronaut()[::5, ::6]
|
||||
GRAY_IMAGE = data.camera()[::5, ::5]
|
||||
|
||||
SIGMA = 3
|
||||
smooth = partial(filters.gaussian, sigma=SIGMA)
|
||||
assert_allclose = partial(np.testing.assert_allclose, atol=1e-8)
|
||||
|
||||
|
||||
@adapt_rgb(each_channel)
|
||||
def edges_each(image):
|
||||
return filters.sobel(image)
|
||||
|
||||
|
||||
@adapt_rgb(each_channel)
|
||||
def smooth_each(image, sigma):
|
||||
return filters.gaussian(image, sigma=sigma)
|
||||
|
||||
|
||||
@adapt_rgb(each_channel)
|
||||
def mask_each(image, mask):
|
||||
result = image.copy()
|
||||
result[mask] = 0
|
||||
return result
|
||||
|
||||
|
||||
@adapt_rgb(hsv_value)
|
||||
def edges_hsv(image):
|
||||
return filters.sobel(image)
|
||||
|
||||
|
||||
@adapt_rgb(hsv_value)
|
||||
def smooth_hsv(image, sigma):
|
||||
return filters.gaussian(image, sigma=sigma)
|
||||
|
||||
|
||||
@adapt_rgb(hsv_value)
|
||||
def edges_hsv_uint(image):
|
||||
return img_as_uint(filters.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), filters.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(filters.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_each_channel_with_asymmetric_kernel():
|
||||
mask = np.triu(np.ones(COLOR_IMAGE.shape[:2], dtype=bool))
|
||||
mask_each(COLOR_IMAGE, mask)
|
||||
|
||||
|
||||
def test_hsv_value():
|
||||
filtered = edges_hsv(COLOR_IMAGE)
|
||||
value = color.rgb2hsv(COLOR_IMAGE)[:, :, 2]
|
||||
assert_allclose(color.rgb2hsv(filtered)[:, :, 2], filters.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, filters.sobel(value), rtol=1e-5, atol=1e-5)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,318 @@
|
||||
import itertools
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from numpy.testing import (
|
||||
assert_array_almost_equal,
|
||||
assert_array_equal,
|
||||
assert_no_warnings,
|
||||
assert_warns,
|
||||
)
|
||||
|
||||
from skimage._shared.testing import expected_warnings
|
||||
from skimage.color.colorconv import hsv2rgb, rgb2hsv
|
||||
from skimage.color.colorlabel import label2rgb
|
||||
|
||||
|
||||
def test_shape_mismatch():
|
||||
image = np.ones((3, 3))
|
||||
label = np.ones((2, 2))
|
||||
with pytest.raises(ValueError):
|
||||
label2rgb(image, label, bg_label=-1)
|
||||
|
||||
|
||||
def test_wrong_kind():
|
||||
label = np.ones((3, 3))
|
||||
# Must not raise an error.
|
||||
label2rgb(label, bg_label=-1)
|
||||
# kind='foo' is wrong.
|
||||
with pytest.raises(ValueError):
|
||||
label2rgb(label, kind='foo', bg_label=-1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("channel_axis", [0, 1, -1])
|
||||
def test_uint_image(channel_axis):
|
||||
img = np.random.randint(0, 255, (10, 10), dtype=np.uint8)
|
||||
labels = np.zeros((10, 10), dtype=np.int64)
|
||||
labels[1:3, 1:3] = 1
|
||||
labels[6:9, 6:9] = 2
|
||||
output = label2rgb(labels, image=img, bg_label=0, channel_axis=channel_axis)
|
||||
# Make sure that the output is made of floats and in the correct range
|
||||
assert np.issubdtype(output.dtype, np.floating)
|
||||
assert output.max() <= 1
|
||||
|
||||
# size 3 (RGB) along the specified channel_axis
|
||||
new_axis = channel_axis % output.ndim
|
||||
assert output.shape[new_axis] == 3
|
||||
|
||||
|
||||
def test_rgb():
|
||||
image = np.ones((1, 3))
|
||||
label = np.arange(3).reshape(1, -1)
|
||||
colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1)]
|
||||
# Set alphas just in case the defaults change
|
||||
rgb = label2rgb(
|
||||
label, image=image, colors=colors, alpha=1, image_alpha=1, bg_label=-1
|
||||
)
|
||||
assert_array_almost_equal(rgb, [colors])
|
||||
|
||||
|
||||
def test_alpha():
|
||||
image = np.random.uniform(size=(3, 3))
|
||||
label = np.random.randint(0, 9, size=(3, 3))
|
||||
# If we set `alpha = 0`, then rgb should match image exactly.
|
||||
rgb = label2rgb(label, image=image, alpha=0, image_alpha=1, bg_label=-1)
|
||||
assert_array_almost_equal(rgb[..., 0], image)
|
||||
assert_array_almost_equal(rgb[..., 1], image)
|
||||
assert_array_almost_equal(rgb[..., 2], image)
|
||||
|
||||
|
||||
def test_no_input_image():
|
||||
label = np.arange(3).reshape(1, -1)
|
||||
colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1)]
|
||||
rgb = label2rgb(label, colors=colors, bg_label=-1)
|
||||
assert_array_almost_equal(rgb, [colors])
|
||||
|
||||
|
||||
def test_image_alpha():
|
||||
image = np.random.uniform(size=(1, 3))
|
||||
label = np.arange(3).reshape(1, -1)
|
||||
colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1)]
|
||||
# If we set `image_alpha = 0`, then rgb should match label colors exactly.
|
||||
rgb = label2rgb(
|
||||
label, image=image, colors=colors, alpha=1, image_alpha=0, bg_label=-1
|
||||
)
|
||||
assert_array_almost_equal(rgb, [colors])
|
||||
|
||||
|
||||
def test_color_names():
|
||||
image = np.ones((1, 3))
|
||||
label = np.arange(3).reshape(1, -1)
|
||||
cnames = ['red', 'lime', 'blue']
|
||||
colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1)]
|
||||
# Set alphas just in case the defaults change
|
||||
rgb = label2rgb(
|
||||
label, image=image, colors=cnames, alpha=1, image_alpha=1, bg_label=-1
|
||||
)
|
||||
assert_array_almost_equal(rgb, [colors])
|
||||
|
||||
|
||||
def test_bg_and_color_cycle():
|
||||
image = np.zeros((1, 10)) # dummy image
|
||||
label = np.arange(10).reshape(1, -1)
|
||||
colors = [(1, 0, 0), (0, 0, 1)]
|
||||
bg_color = (0, 0, 0)
|
||||
rgb = label2rgb(
|
||||
label, image=image, bg_label=0, bg_color=bg_color, colors=colors, alpha=1
|
||||
)
|
||||
assert_array_almost_equal(rgb[0, 0], bg_color)
|
||||
for pixel, color in zip(rgb[0, 1:], itertools.cycle(colors)):
|
||||
assert_array_almost_equal(pixel, color)
|
||||
|
||||
|
||||
def test_negative_labels():
|
||||
labels = np.array([0, -1, -2, 0])
|
||||
rout = np.array(
|
||||
[(0.0, 0.0, 0.0), (0.0, 0.0, 1.0), (1.0, 0.0, 0.0), (0.0, 0.0, 0.0)]
|
||||
)
|
||||
assert_array_almost_equal(
|
||||
rout, label2rgb(labels, bg_label=0, alpha=1, image_alpha=1)
|
||||
)
|
||||
|
||||
|
||||
def test_nonconsecutive():
|
||||
labels = np.array([0, 2, 4, 0])
|
||||
colors = [(1, 0, 0), (0, 0, 1)]
|
||||
rout = np.array(
|
||||
[(1.0, 0.0, 0.0), (0.0, 0.0, 1.0), (1.0, 0.0, 0.0), (1.0, 0.0, 0.0)]
|
||||
)
|
||||
assert_array_almost_equal(
|
||||
rout, label2rgb(labels, colors=colors, alpha=1, image_alpha=1, bg_label=-1)
|
||||
)
|
||||
|
||||
|
||||
def test_label_consistency():
|
||||
"""Assert that the same labels map to the same colors."""
|
||||
label_1 = np.arange(5).reshape(1, -1)
|
||||
label_2 = np.array([0, 1])
|
||||
colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 0), (1, 0, 1)]
|
||||
# Set alphas just in case the defaults change
|
||||
rgb_1 = label2rgb(label_1, colors=colors, bg_label=-1)
|
||||
rgb_2 = label2rgb(label_2, colors=colors, bg_label=-1)
|
||||
for label_id in label_2.flat:
|
||||
assert_array_almost_equal(
|
||||
rgb_1[label_1 == label_id], rgb_2[label_2 == label_id]
|
||||
)
|
||||
|
||||
|
||||
def test_leave_labels_alone():
|
||||
labels = np.array([-1, 0, 1])
|
||||
labels_saved = labels.copy()
|
||||
|
||||
label2rgb(labels, bg_label=-1)
|
||||
label2rgb(labels, bg_label=1)
|
||||
assert_array_equal(labels, labels_saved)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("channel_axis", [0, 1, -1])
|
||||
def test_avg(channel_axis):
|
||||
# label image
|
||||
label_field = np.array([[1, 1, 1, 2], [1, 2, 2, 2], [3, 3, 4, 4]], dtype=np.uint8)
|
||||
|
||||
# color image
|
||||
r = np.array([[1.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 1.0], [0.0, 0.0, 0.0, 0.0]])
|
||||
g = np.array([[0.0, 0.0, 0.0, 1.0], [1.0, 1.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0]])
|
||||
b = np.array([[0.0, 0.0, 0.0, 1.0], [0.0, 1.0, 1.0, 1.0], [0.0, 0.0, 1.0, 1.0]])
|
||||
image = np.dstack((r, g, b))
|
||||
|
||||
# reference label-colored image
|
||||
rout = np.array([[0.5, 0.5, 0.5, 0.5], [0.5, 0.5, 0.5, 0.5], [0.0, 0.0, 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, 0.0, 0.0]]
|
||||
)
|
||||
bout = np.array([[0.0, 0.0, 0.0, 1.0], [0.0, 1.0, 1.0, 1.0], [0.0, 0.0, 1.0, 1.0]])
|
||||
expected_out = np.dstack((rout, gout, bout))
|
||||
|
||||
# test standard averaging
|
||||
_image = np.moveaxis(image, source=-1, destination=channel_axis)
|
||||
out = label2rgb(
|
||||
label_field, _image, kind='avg', bg_label=-1, channel_axis=channel_axis
|
||||
)
|
||||
out = np.moveaxis(out, source=channel_axis, destination=-1)
|
||||
assert_array_equal(out, expected_out)
|
||||
|
||||
# test averaging with custom background value
|
||||
out_bg = label2rgb(
|
||||
label_field,
|
||||
_image,
|
||||
bg_label=2,
|
||||
bg_color=(0, 0, 0),
|
||||
kind='avg',
|
||||
channel_axis=channel_axis,
|
||||
)
|
||||
out_bg = np.moveaxis(out_bg, source=channel_axis, destination=-1)
|
||||
expected_out_bg = expected_out.copy()
|
||||
expected_out_bg[label_field == 2] = 0
|
||||
assert_array_equal(out_bg, expected_out_bg)
|
||||
|
||||
# test default background color
|
||||
out_bg = label2rgb(
|
||||
label_field, _image, bg_label=2, kind='avg', channel_axis=channel_axis
|
||||
)
|
||||
out_bg = np.moveaxis(out_bg, source=channel_axis, destination=-1)
|
||||
assert_array_equal(out_bg, expected_out_bg)
|
||||
|
||||
|
||||
def test_negative_intensity():
|
||||
labels = np.arange(100).reshape(10, 10)
|
||||
image = np.full((10, 10), -1, dtype='float64')
|
||||
assert_warns(UserWarning, label2rgb, labels, image, bg_label=-1)
|
||||
|
||||
|
||||
def test_bg_color_rgb_string():
|
||||
img = np.random.randint(0, 255, (10, 10), dtype=np.uint8)
|
||||
labels = np.zeros((10, 10), dtype=np.int64)
|
||||
labels[1:3, 1:3] = 1
|
||||
labels[6:9, 6:9] = 2
|
||||
output = label2rgb(labels, image=img, alpha=0.9, bg_label=0, bg_color='red')
|
||||
assert output[0, 0, 0] > 0.9 # red channel
|
||||
|
||||
|
||||
def test_avg_with_2d_image():
|
||||
img = np.random.randint(0, 255, (10, 10), dtype=np.uint8)
|
||||
labels = np.zeros((10, 10), dtype=np.int64)
|
||||
labels[1:3, 1:3] = 1
|
||||
labels[6:9, 6:9] = 2
|
||||
assert_no_warnings(label2rgb, labels, image=img, bg_label=0, kind='avg')
|
||||
|
||||
|
||||
@pytest.mark.parametrize('image_type', ['rgb', 'gray', None])
|
||||
def test_label2rgb_nd(image_type):
|
||||
# validate 1D and 3D cases by testing their output relative to the 2D case
|
||||
shape = (10, 10)
|
||||
if image_type == 'rgb':
|
||||
img = np.random.randint(0, 255, shape + (3,), dtype=np.uint8)
|
||||
elif image_type == 'gray':
|
||||
img = np.random.randint(0, 255, shape, dtype=np.uint8)
|
||||
else:
|
||||
img = None
|
||||
|
||||
# add a couple of rectangular labels
|
||||
labels = np.zeros(shape, dtype=np.int64)
|
||||
# Note: Have to choose labels here so that the 1D slice below also contains
|
||||
# both label values. Otherwise the labeled colors will not match.
|
||||
labels[2:-2, 1:3] = 1
|
||||
labels[3:-3, 6:9] = 2
|
||||
|
||||
# label in the 2D case (correct 2D output is tested in other functions)
|
||||
labeled_2d = label2rgb(labels, image=img, bg_label=0)
|
||||
|
||||
# labeling a single line gives an equivalent result
|
||||
image_1d = img[5] if image_type is not None else None
|
||||
labeled_1d = label2rgb(labels[5], image=image_1d, bg_label=0)
|
||||
expected = labeled_2d[5]
|
||||
assert_array_equal(labeled_1d, expected)
|
||||
|
||||
# Labeling a 3D stack of duplicates gives the same result in each plane
|
||||
image_3d = np.stack((img,) * 4) if image_type is not None else None
|
||||
labels_3d = np.stack((labels,) * 4)
|
||||
labeled_3d = label2rgb(labels_3d, image=image_3d, bg_label=0)
|
||||
for labeled_plane in labeled_3d:
|
||||
assert_array_equal(labeled_plane, labeled_2d)
|
||||
|
||||
|
||||
def test_label2rgb_shape_errors():
|
||||
img = np.random.randint(0, 255, (10, 10, 3), dtype=np.uint8)
|
||||
labels = np.zeros((10, 10), dtype=np.int64)
|
||||
labels[2:5, 2:5] = 1
|
||||
|
||||
# mismatched 2D shape
|
||||
with pytest.raises(ValueError):
|
||||
label2rgb(labels, img[1:])
|
||||
|
||||
# too many axes in img
|
||||
with pytest.raises(ValueError):
|
||||
label2rgb(labels, img[..., np.newaxis])
|
||||
|
||||
# too many channels along the last axis
|
||||
with pytest.raises(ValueError):
|
||||
label2rgb(labels, np.concatenate((img, img), axis=-1))
|
||||
|
||||
|
||||
def test_overlay_full_saturation():
|
||||
rgb_img = np.random.uniform(size=(10, 10, 3))
|
||||
labels = np.ones((10, 10), dtype=np.int64)
|
||||
labels[5:, 5:] = 2
|
||||
labels[:3, :3] = 0
|
||||
alpha = 0.3
|
||||
rgb = label2rgb(labels, image=rgb_img, alpha=alpha, bg_label=0, saturation=1)
|
||||
# check that rgb part of input image is preserved, where labels=0
|
||||
assert_array_almost_equal(rgb_img[:3, :3] * (1 - alpha), rgb[:3, :3])
|
||||
|
||||
|
||||
def test_overlay_custom_saturation():
|
||||
rgb_img = np.random.uniform(size=(10, 10, 3))
|
||||
labels = np.ones((10, 10), dtype=np.int64)
|
||||
labels[5:, 5:] = 2
|
||||
labels[:3, :3] = 0
|
||||
alpha = 0.3
|
||||
saturation = 0.3
|
||||
rgb = label2rgb(
|
||||
labels, image=rgb_img, alpha=alpha, bg_label=0, saturation=saturation
|
||||
)
|
||||
|
||||
hsv = rgb2hsv(rgb_img)
|
||||
hsv[..., 1] *= saturation
|
||||
saturaded_img = hsv2rgb(hsv)
|
||||
|
||||
# check that rgb part of input image is saturated, where labels=0
|
||||
assert_array_almost_equal(saturaded_img[:3, :3] * (1 - alpha), rgb[:3, :3])
|
||||
|
||||
|
||||
def test_saturation_warning():
|
||||
rgb_img = np.random.uniform(size=(10, 10, 3))
|
||||
labels = np.ones((10, 10), dtype=np.int64)
|
||||
with expected_warnings(["saturation must be in range"]):
|
||||
label2rgb(labels, image=rgb_img, bg_label=0, saturation=2)
|
||||
with expected_warnings(["saturation must be in range"]):
|
||||
label2rgb(labels, image=rgb_img, bg_label=0, saturation=-1)
|
||||
@@ -0,0 +1,299 @@
|
||||
"""Test for correctness of color distance functions"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from numpy.testing import assert_allclose, assert_almost_equal, assert_equal
|
||||
|
||||
from skimage._shared.testing import fetch
|
||||
from skimage._shared.utils import _supported_float_type
|
||||
from skimage.color.delta_e import (
|
||||
deltaE_cie76,
|
||||
deltaE_ciede94,
|
||||
deltaE_ciede2000,
|
||||
deltaE_cmc,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("channel_axis", [0, 1, -1])
|
||||
@pytest.mark.parametrize('dtype', [np.float32, np.float64])
|
||||
def test_ciede2000_dE(dtype, channel_axis):
|
||||
data = load_ciede2000_data()
|
||||
N = len(data)
|
||||
lab1 = np.zeros((N, 3), dtype=dtype)
|
||||
lab1[:, 0] = data['L1']
|
||||
lab1[:, 1] = data['a1']
|
||||
lab1[:, 2] = data['b1']
|
||||
|
||||
lab2 = np.zeros((N, 3), dtype=dtype)
|
||||
lab2[:, 0] = data['L2']
|
||||
lab2[:, 1] = data['a2']
|
||||
lab2[:, 2] = data['b2']
|
||||
|
||||
lab1 = np.moveaxis(lab1, source=-1, destination=channel_axis)
|
||||
lab2 = np.moveaxis(lab2, source=-1, destination=channel_axis)
|
||||
dE2 = deltaE_ciede2000(lab1, lab2, channel_axis=channel_axis)
|
||||
assert dE2.dtype == _supported_float_type(dtype)
|
||||
|
||||
assert_allclose(dE2, data['dE'], rtol=1e-2)
|
||||
|
||||
|
||||
def load_ciede2000_data():
|
||||
dtype = [
|
||||
('pair', int),
|
||||
('1', int),
|
||||
('L1', float),
|
||||
('a1', float),
|
||||
('b1', float),
|
||||
('a1_prime', float),
|
||||
('C1_prime', float),
|
||||
('h1_prime', float),
|
||||
('hbar_prime', float),
|
||||
('G', float),
|
||||
('T', float),
|
||||
('SL', float),
|
||||
('SC', float),
|
||||
('SH', float),
|
||||
('RT', float),
|
||||
('dE', float),
|
||||
('2', int),
|
||||
('L2', float),
|
||||
('a2', float),
|
||||
('b2', float),
|
||||
('a2_prime', float),
|
||||
('C2_prime', float),
|
||||
('h2_prime', float),
|
||||
]
|
||||
|
||||
# note: ciede_test_data.txt contains several intermediate quantities
|
||||
path = fetch('color/tests/ciede2000_test_data.txt')
|
||||
return np.loadtxt(path, dtype=dtype)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("channel_axis", [0, 1, -1])
|
||||
@pytest.mark.parametrize('dtype', [np.float32, np.float64])
|
||||
def test_cie76(dtype, channel_axis):
|
||||
data = load_ciede2000_data()
|
||||
N = len(data)
|
||||
lab1 = np.zeros((N, 3), dtype=dtype)
|
||||
lab1[:, 0] = data['L1']
|
||||
lab1[:, 1] = data['a1']
|
||||
lab1[:, 2] = data['b1']
|
||||
|
||||
lab2 = np.zeros((N, 3), dtype=dtype)
|
||||
lab2[:, 0] = data['L2']
|
||||
lab2[:, 1] = data['a2']
|
||||
lab2[:, 2] = data['b2']
|
||||
|
||||
lab1 = np.moveaxis(lab1, source=-1, destination=channel_axis)
|
||||
lab2 = np.moveaxis(lab2, source=-1, destination=channel_axis)
|
||||
dE2 = deltaE_cie76(lab1, lab2, channel_axis=channel_axis)
|
||||
assert dE2.dtype == _supported_float_type(dtype)
|
||||
oracle = np.array(
|
||||
[
|
||||
4.00106328,
|
||||
6.31415011,
|
||||
9.1776999,
|
||||
2.06270077,
|
||||
2.36957073,
|
||||
2.91529271,
|
||||
2.23606798,
|
||||
2.23606798,
|
||||
4.98000036,
|
||||
4.9800004,
|
||||
4.98000044,
|
||||
4.98000049,
|
||||
4.98000036,
|
||||
4.9800004,
|
||||
4.98000044,
|
||||
3.53553391,
|
||||
36.86800781,
|
||||
31.91002977,
|
||||
30.25309901,
|
||||
27.40894015,
|
||||
0.89242934,
|
||||
0.7972,
|
||||
0.8583065,
|
||||
0.82982507,
|
||||
3.1819238,
|
||||
2.21334297,
|
||||
1.53890382,
|
||||
4.60630929,
|
||||
6.58467989,
|
||||
3.88641412,
|
||||
1.50514845,
|
||||
2.3237848,
|
||||
0.94413208,
|
||||
1.31910843,
|
||||
]
|
||||
)
|
||||
rtol = 1e-5 if dtype == np.float32 else 1e-8
|
||||
assert_allclose(dE2, oracle, rtol=rtol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("channel_axis", [0, 1, -1])
|
||||
@pytest.mark.parametrize('dtype', [np.float32, np.float64])
|
||||
def test_ciede94(dtype, channel_axis):
|
||||
data = load_ciede2000_data()
|
||||
N = len(data)
|
||||
lab1 = np.zeros((N, 3), dtype=dtype)
|
||||
lab1[:, 0] = data['L1']
|
||||
lab1[:, 1] = data['a1']
|
||||
lab1[:, 2] = data['b1']
|
||||
|
||||
lab2 = np.zeros((N, 3), dtype=dtype)
|
||||
lab2[:, 0] = data['L2']
|
||||
lab2[:, 1] = data['a2']
|
||||
lab2[:, 2] = data['b2']
|
||||
|
||||
lab1 = np.moveaxis(lab1, source=-1, destination=channel_axis)
|
||||
lab2 = np.moveaxis(lab2, source=-1, destination=channel_axis)
|
||||
dE2 = deltaE_ciede94(lab1, lab2, channel_axis=channel_axis)
|
||||
assert dE2.dtype == _supported_float_type(dtype)
|
||||
oracle = np.array(
|
||||
[
|
||||
1.39503887,
|
||||
1.93410055,
|
||||
2.45433566,
|
||||
0.68449187,
|
||||
0.6695627,
|
||||
0.69194527,
|
||||
2.23606798,
|
||||
2.03163832,
|
||||
4.80069441,
|
||||
4.80069445,
|
||||
4.80069449,
|
||||
4.80069453,
|
||||
4.80069441,
|
||||
4.80069445,
|
||||
4.80069449,
|
||||
3.40774352,
|
||||
34.6891632,
|
||||
29.44137328,
|
||||
27.91408781,
|
||||
24.93766082,
|
||||
0.82213163,
|
||||
0.71658427,
|
||||
0.8048753,
|
||||
0.75284394,
|
||||
1.39099471,
|
||||
1.24808929,
|
||||
1.29795787,
|
||||
1.82045088,
|
||||
2.55613309,
|
||||
1.42491303,
|
||||
1.41945261,
|
||||
2.3225685,
|
||||
0.93853308,
|
||||
1.30654464,
|
||||
]
|
||||
)
|
||||
rtol = 1e-5 if dtype == np.float32 else 1e-8
|
||||
assert_allclose(dE2, oracle, rtol=rtol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("channel_axis", [0, 1, -1])
|
||||
@pytest.mark.parametrize('dtype', [np.float32, np.float64])
|
||||
def test_cmc(dtype, channel_axis):
|
||||
data = load_ciede2000_data()
|
||||
N = len(data)
|
||||
lab1 = np.zeros((N, 3), dtype=dtype)
|
||||
lab1[:, 0] = data['L1']
|
||||
lab1[:, 1] = data['a1']
|
||||
lab1[:, 2] = data['b1']
|
||||
|
||||
lab2 = np.zeros((N, 3), dtype=dtype)
|
||||
lab2[:, 0] = data['L2']
|
||||
lab2[:, 1] = data['a2']
|
||||
lab2[:, 2] = data['b2']
|
||||
|
||||
lab1 = np.moveaxis(lab1, source=-1, destination=channel_axis)
|
||||
lab2 = np.moveaxis(lab2, source=-1, destination=channel_axis)
|
||||
dE2 = deltaE_cmc(lab1, lab2, channel_axis=channel_axis)
|
||||
assert dE2.dtype == _supported_float_type(dtype)
|
||||
oracle = np.array(
|
||||
[
|
||||
1.73873611,
|
||||
2.49660844,
|
||||
3.30494501,
|
||||
0.85735576,
|
||||
0.88332927,
|
||||
0.97822692,
|
||||
3.50480874,
|
||||
2.87930032,
|
||||
6.5783807,
|
||||
6.57838075,
|
||||
6.5783808,
|
||||
6.57838086,
|
||||
6.67492321,
|
||||
6.67492326,
|
||||
6.67492331,
|
||||
4.66852997,
|
||||
42.10875485,
|
||||
39.45889064,
|
||||
38.36005919,
|
||||
33.93663807,
|
||||
1.14400168,
|
||||
1.00600419,
|
||||
1.11302547,
|
||||
1.05335328,
|
||||
1.42822951,
|
||||
1.2548143,
|
||||
1.76838061,
|
||||
2.02583367,
|
||||
3.08695508,
|
||||
1.74893533,
|
||||
1.90095165,
|
||||
1.70258148,
|
||||
1.80317207,
|
||||
2.44934417,
|
||||
]
|
||||
)
|
||||
rtol = 1e-5 if dtype == np.float32 else 1e-8
|
||||
assert_allclose(dE2, oracle, rtol=rtol)
|
||||
|
||||
# Equal or close colors make `delta_e.get_dH2` function to return
|
||||
# negative values resulting in NaNs when passed to sqrt (see #1908
|
||||
# issue on Github):
|
||||
lab1 = lab2
|
||||
expected = np.zeros_like(oracle)
|
||||
assert_almost_equal(
|
||||
deltaE_cmc(lab1, lab2, channel_axis=channel_axis), expected, decimal=6
|
||||
)
|
||||
|
||||
lab2[0, 0] += np.finfo(float).eps
|
||||
assert_almost_equal(
|
||||
deltaE_cmc(lab1, lab2, channel_axis=channel_axis), expected, decimal=6
|
||||
)
|
||||
|
||||
|
||||
def test_cmc_single_item():
|
||||
# Single item case:
|
||||
lab1 = lab2 = np.array([0.0, 1.59607713, 0.87755709])
|
||||
assert_equal(deltaE_cmc(lab1, lab2), 0)
|
||||
|
||||
lab2[0] += np.finfo(float).eps
|
||||
assert_equal(deltaE_cmc(lab1, lab2), 0)
|
||||
|
||||
|
||||
def test_single_color_cie76():
|
||||
lab1 = (0.5, 0.5, 0.5)
|
||||
lab2 = (0.4, 0.4, 0.4)
|
||||
deltaE_cie76(lab1, lab2)
|
||||
|
||||
|
||||
def test_single_color_ciede94():
|
||||
lab1 = (0.5, 0.5, 0.5)
|
||||
lab2 = (0.4, 0.4, 0.4)
|
||||
deltaE_ciede94(lab1, lab2)
|
||||
|
||||
|
||||
def test_single_color_ciede2000():
|
||||
lab1 = (0.5, 0.5, 0.5)
|
||||
lab2 = (0.4, 0.4, 0.4)
|
||||
deltaE_ciede2000(lab1, lab2)
|
||||
|
||||
|
||||
def test_single_color_cmc():
|
||||
lab1 = (0.5, 0.5, 0.5)
|
||||
lab2 = (0.4, 0.4, 0.4)
|
||||
deltaE_cmc(lab1, lab2)
|
||||
@@ -0,0 +1,17 @@
|
||||
import pytest
|
||||
|
||||
# List of files that pytest should ignore
|
||||
collect_ignore = [
|
||||
"io/_plugins",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def handle_np2():
|
||||
# TODO: remove when we require numpy >= 2
|
||||
try:
|
||||
import numpy as np
|
||||
|
||||
np.set_printoptions(legacy="1.21")
|
||||
except ImportError:
|
||||
pass
|
||||
@@ -0,0 +1,9 @@
|
||||
This directory contains sample data from scikit-image.
|
||||
|
||||
By default, it only contains a small subset of the entire dataset.
|
||||
|
||||
The full detaset can be downloaded by using the following commands from
|
||||
a python console.
|
||||
|
||||
>>> from skimage.data import download_all
|
||||
>>> download_all()
|
||||
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
Test images and datasets.
|
||||
|
||||
A curated set of general purpose and scientific images used in tests, examples,
|
||||
and documentation.
|
||||
|
||||
Newer datasets are no longer included as part of the package, but are
|
||||
downloaded on demand. To make data available offline, use :func:`download_all`.
|
||||
|
||||
"""
|
||||
|
||||
import lazy_loader as lazy
|
||||
|
||||
__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__)
|
||||
@@ -0,0 +1,88 @@
|
||||
__all__ = [
|
||||
'astronaut',
|
||||
'binary_blobs',
|
||||
'brain',
|
||||
'brick',
|
||||
'camera',
|
||||
'cat',
|
||||
'cell',
|
||||
'cells3d',
|
||||
'checkerboard',
|
||||
'chelsea',
|
||||
'clock',
|
||||
'coffee',
|
||||
'coins',
|
||||
'colorwheel',
|
||||
'data_dir',
|
||||
'download_all',
|
||||
'eagle',
|
||||
'file_hash',
|
||||
'grass',
|
||||
'gravel',
|
||||
'horse',
|
||||
'hubble_deep_field',
|
||||
'human_mitosis',
|
||||
'immunohistochemistry',
|
||||
'kidney',
|
||||
'lbp_frontal_face_cascade_filename',
|
||||
'lfw_subset',
|
||||
'lily',
|
||||
'logo',
|
||||
'microaneurysms',
|
||||
'moon',
|
||||
'nickel_solidification',
|
||||
'page',
|
||||
'protein_transport',
|
||||
'retina',
|
||||
'rocket',
|
||||
'shepp_logan_phantom',
|
||||
'skin',
|
||||
'stereo_motorcycle',
|
||||
'text',
|
||||
'vortex',
|
||||
]
|
||||
|
||||
from ._binary_blobs import binary_blobs
|
||||
from ._fetchers import (
|
||||
astronaut,
|
||||
brain,
|
||||
brick,
|
||||
camera,
|
||||
cat,
|
||||
cell,
|
||||
cells3d,
|
||||
checkerboard,
|
||||
chelsea,
|
||||
clock,
|
||||
coffee,
|
||||
coins,
|
||||
colorwheel,
|
||||
data_dir,
|
||||
download_all,
|
||||
eagle,
|
||||
file_hash,
|
||||
grass,
|
||||
gravel,
|
||||
horse,
|
||||
hubble_deep_field,
|
||||
human_mitosis,
|
||||
immunohistochemistry,
|
||||
kidney,
|
||||
lbp_frontal_face_cascade_filename,
|
||||
lfw_subset,
|
||||
lily,
|
||||
logo,
|
||||
microaneurysms,
|
||||
moon,
|
||||
nickel_solidification,
|
||||
page,
|
||||
palisades_of_vogt,
|
||||
protein_transport,
|
||||
retina,
|
||||
rocket,
|
||||
shepp_logan_phantom,
|
||||
skin,
|
||||
stereo_motorcycle,
|
||||
text,
|
||||
vortex,
|
||||
)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,61 @@
|
||||
import numpy as np
|
||||
|
||||
from .._shared.filters import gaussian
|
||||
|
||||
|
||||
def binary_blobs(
|
||||
length=512, blob_size_fraction=0.1, n_dim=2, volume_fraction=0.5, rng=None
|
||||
):
|
||||
"""
|
||||
Generate synthetic binary image with several rounded blob-like objects.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
length : int, optional
|
||||
Linear size of output image.
|
||||
blob_size_fraction : float, optional
|
||||
Typical linear size of blob, as a fraction of ``length``, should be
|
||||
smaller than 1.
|
||||
n_dim : int, optional
|
||||
Number of dimensions of output image.
|
||||
volume_fraction : float, default 0.5
|
||||
Fraction of image pixels covered by the blobs (where the output is 1).
|
||||
Should be in [0, 1].
|
||||
rng : {`numpy.random.Generator`, int}, optional
|
||||
Pseudo-random number generator.
|
||||
By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`).
|
||||
If `rng` is an int, it is used to seed the generator.
|
||||
|
||||
Returns
|
||||
-------
|
||||
blobs : ndarray of bools
|
||||
Output binary image
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from skimage import data
|
||||
>>> data.binary_blobs(length=5, blob_size_fraction=0.2) # doctest: +SKIP
|
||||
array([[ True, False, True, True, True],
|
||||
[ True, True, True, False, True],
|
||||
[False, True, False, True, True],
|
||||
[ True, False, False, True, True],
|
||||
[ True, False, False, False, True]])
|
||||
>>> blobs = data.binary_blobs(length=256, blob_size_fraction=0.1)
|
||||
>>> # Finer structures
|
||||
>>> blobs = data.binary_blobs(length=256, blob_size_fraction=0.05)
|
||||
>>> # Blobs cover a smaller volume fraction of the image
|
||||
>>> blobs = data.binary_blobs(length=256, volume_fraction=0.3)
|
||||
|
||||
"""
|
||||
|
||||
rs = np.random.default_rng(rng)
|
||||
shape = tuple([length] * n_dim)
|
||||
mask = np.zeros(shape)
|
||||
n_pts = max(int(1.0 / blob_size_fraction) ** n_dim, 1)
|
||||
points = (length * rs.random((n_dim, n_pts))).astype(int)
|
||||
mask[tuple(indices for indices in points)] = 1
|
||||
mask = gaussian(
|
||||
mask, sigma=0.25 * length * blob_size_fraction, preserve_range=False
|
||||
)
|
||||
threshold = np.percentile(mask, 100 * (1 - volume_fraction))
|
||||
return np.logical_not(mask < threshold)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,187 @@
|
||||
# flake8: noqa
|
||||
|
||||
# This minimal dataset was available as part of
|
||||
# scikit-image 0.15 and will be retained until
|
||||
# further notice.
|
||||
# Testing data and additional datasets should only
|
||||
# be made available by pooch
|
||||
legacy_datasets = [
|
||||
'astronaut.png',
|
||||
'brick.png',
|
||||
'camera.png',
|
||||
'chessboard_GRAY.png',
|
||||
'chessboard_RGB.png',
|
||||
'chelsea.png',
|
||||
'clock_motion.png',
|
||||
'coffee.png',
|
||||
'coins.png',
|
||||
'color.png',
|
||||
'cell.png',
|
||||
'grass.png',
|
||||
'gravel.png',
|
||||
'horse.png',
|
||||
'hubble_deep_field.jpg',
|
||||
'ihc.png',
|
||||
'lbpcascade_frontalface_opencv.xml',
|
||||
'lfw_subset.npy',
|
||||
'logo.png',
|
||||
'microaneurysms.png',
|
||||
'moon.png',
|
||||
'page.png',
|
||||
'text.png',
|
||||
'retina.jpg',
|
||||
'rocket.jpg',
|
||||
'phantom.png',
|
||||
'motorcycle_disp.npz',
|
||||
'motorcycle_left.png',
|
||||
'motorcycle_right.png',
|
||||
]
|
||||
|
||||
# Registry of datafiles that can be downloaded along with their SHA256 hashes
|
||||
# To generate the SHA256 hash, use the command
|
||||
# openssl sha256 filename
|
||||
registry = {
|
||||
"color/tests/data/lab_array_a_10.npy": "a3ef76f1530e374f9121020f1f220bc89767dc866f4bbd1b1f47e5b84891a38c",
|
||||
"color/tests/data/lab_array_a_2.npy": "793d5981cbffceb14b5fb589f998a2b1acdb5ff9c14d364c8e9e8bd45a80b275",
|
||||
"color/tests/data/lab_array_a_r.npy": "3d3613da109d0c87827525fc49b58111aefc12438fa6426654979f66807b9227",
|
||||
"color/tests/data/lab_array_b_10.npy": "e8d648b28077c1bfcef55ec6dc8679819612b56a01647f8c0a78625bb06f99b6",
|
||||
"color/tests/data/lab_array_b_2.npy": "da9c6aa99e4ab3af8ec3107bbf11647cc483a0760285dd5c9fb66988be393ca1",
|
||||
"color/tests/data/lab_array_b_r.npy": "d9eee96f4d65a2fbba82039508aac8c18304752ee8e33233e2a013e65bb91464",
|
||||
"color/tests/data/lab_array_c_10.npy": "88b4ff2a2d2c4f48e7bb265609221d4b9ef439a4e2d8a86989696bfdb47790e6",
|
||||
"color/tests/data/lab_array_c_2.npy": "e1b8acfdc7284ab9cd339de66948134304073b6f734ecf9ad42f8297b83d3405",
|
||||
"color/tests/data/lab_array_c_r.npy": "09ffba2ed69e467864fea883493cd2d2706da028433464e3e858a8086842867e",
|
||||
"color/tests/data/lab_array_d50_10.npy": "42e2ff26cb10e2a98fcf1bc06c2483302ff4fabf971fe8d49b530f490b5d24c7",
|
||||
"color/tests/data/lab_array_d50_2.npy": "4aa03b7018ff7276643d3c082123cf07304f9d8d898ae92a5756a86955de4faf",
|
||||
"color/tests/data/lab_array_d50_r.npy": "57db02009f9a68dade33ce1ecffead0418d8ac8113b2a589fc02a20e6bf7e799",
|
||||
"color/tests/data/lab_array_d55_10.npy": "ab4f21368b6d8351578ab093381c44b49ae87a6b7f25c11aa094b07f215eed7d",
|
||||
"color/tests/data/lab_array_d55_2.npy": "0319723de4632a252bae828b7c96d038fb075a7df05beadfbad653da05efe372",
|
||||
"color/tests/data/lab_array_d55_r.npy": "060ebc446f7b4da4df58a60f0006133dbca735da87ba61854f4a75d28db67a3a",
|
||||
"color/tests/data/lab_array_d65_10.npy": "5cb9e9c384d2577aaf8b7d2d21ff5b505708b80605a2f59d10e89d22c3d308d2",
|
||||
"color/tests/data/lab_array_d65_2.npy": "16e847160f7ba4f19806d8194ed44a6654c9367e5a2cb240aa6e7eece44a6649",
|
||||
"color/tests/data/lab_array_d65_r.npy": "82d0dd7a46741f627b8868793e64cdc2f9944fe1e049b573f752a93760a1577c",
|
||||
"color/tests/data/lab_array_d75_10.npy": "c2d3de5422c785c925926b0c6223aeaf50b9393619d1c30830190d433606cbe1",
|
||||
"color/tests/data/lab_array_d75_2.npy": "c94d53da398d36e076471ff7e0dafcaffc64ce4ba33b4d04849c32d19c87494a",
|
||||
"color/tests/data/lab_array_e_2.npy": "ac05f17a83961b020ceccbdd46bddc86943d43e678dabcc898caf4a1e4be6165",
|
||||
"color/tests/data/luv_array_a_10.npy": "c8af67f9fd64a6e9c610ac0c12c5315a49ca229363f048e5d851409d4a3ae5b6",
|
||||
"color/tests/data/luv_array_a_2.npy": "eaf05dc61f4a70ece367d5e751a14d42b7c397c7b1c2df4cfecec9ddf26e1c1a",
|
||||
"color/tests/data/luv_array_a_r.npy": "2c0891add787ec757601f9c61ad14dd9621dd969af4e32753f2e64df437081b7",
|
||||
"color/tests/data/luv_array_b_10.npy": "a5407736b8a43071139ca178d12cdf930f32f52a0644f0b13f89d8895c8b43db",
|
||||
"color/tests/data/luv_array_b_2.npy": "8e74173d54dc549b6c0ebd1f1d70489d2905cad87744e41ed74384f21f22986d",
|
||||
"color/tests/data/luv_array_b_r.npy": "0a74c41df369cbb5fc0a00c16d60dc6f946ebf144bc5e506545b0d160fa53dfa",
|
||||
"color/tests/data/luv_array_c_10.npy": "3a5f975ffa57f69a1be9e02b153e8161f83040ce3002ea1b0a05b9fbdd0d8ec4",
|
||||
"color/tests/data/luv_array_c_2.npy": "32506cd50ea2181997cb88d3511e275740e8151d6c693cd178f5eafd8b0c6e47",
|
||||
"color/tests/data/luv_array_c_r.npy": "c0fbf98cc0e62ed426ab4d228986d6660089444a7bbfcc64cbb1c632644067bb",
|
||||
"color/tests/data/luv_array_d50_10.npy": "fe223db556222ce3a59198bed3a3324c2c719b8083fb84dc5b00f214b4773b16",
|
||||
"color/tests/data/luv_array_d50_2.npy": "48e8989048904bdf2c3c1ada265c1c29c5eff60f02f848a25cde622982c84901",
|
||||
"color/tests/data/luv_array_d50_r.npy": "f93f0def9c93f872dd10ce4a91fdb3f06eea61ddb6e72387b7669909827d4f9c",
|
||||
"color/tests/data/luv_array_d55_10.npy": "d88d53d2bad230c2331442187712ec52ffdee62bf0f60b200c33411bfed76c60",
|
||||
"color/tests/data/luv_array_d55_2.npy": "c761b40475df591ae9c0475d54ef712d067190ca4652efc6308b69080a652061",
|
||||
"color/tests/data/luv_array_d55_r.npy": "05fbd57e3602ee4d5202b9f18f9b5fc05b545891a9b4456d2a88aa798a5a774a",
|
||||
"color/tests/data/luv_array_d65_10.npy": "41a5452ffac4d31dd579d9528e725432c60d77b5f505d801898d9401429c89bf",
|
||||
"color/tests/data/luv_array_d65_2.npy": "962ce180132c6c11798cbc423b2b204d1d10187670f6eb5dec1058eaad301e0e",
|
||||
"color/tests/data/luv_array_d65_r.npy": "78db8c19af26dd802ce98b039a33855f7c8d6a103a2721d094b1d9c619717449",
|
||||
"color/tests/data/luv_array_d75_10.npy": "e1cc70d56eb6789633d4c2a4059b9533f616a7c8592c9bd342403e41d72f45e4",
|
||||
"color/tests/data/luv_array_d75_2.npy": "07db3bd59bd89de8e5ff62dad786fe5f4b299133495ba9bea30495b375133a98",
|
||||
"color/tests/data/luv_array_e_2.npy": "41b1037d81b267305ffe9e8e97e0affa9fa54b18e60413b01b8f11861cb32213",
|
||||
"color/tests/ciede2000_test_data.txt": "2e005c6f76ddfb7bbcc8f68490f1f7b4b4a2a4b06b36a80c985677a2799c0e40",
|
||||
"data/astronaut.png": "88431cd9653ccd539741b555fb0a46b61558b301d4110412b5bc28b5e3ea6cb5",
|
||||
"data/brick.png": "7966caf324f6ba843118d98f7a07746d22f6a343430add0233eca5f6eaaa8fcf",
|
||||
"data/cell.png": "8d23a7fb81f7cc877cd09f330357fc7f595651306e84e17252f6e0a1b3f61515",
|
||||
"data/camera.png": "b0793d2adda0fa6ae899c03989482bff9a42d3d5690fc7e3648f2795d730c23a",
|
||||
"data/chessboard_GRAY.png": "3e51870774515af4d07d820bd8827364c70839bf9b573c746e485095e893df90",
|
||||
"data/chessboard_RGB.png": "1ac01eff2d4e50f4eda55a2ddecdc28a6576623a58d7a7ef84513c5cc19a0331",
|
||||
"data/chelsea.png": "596aa1e7cb875eb79f437e310381d26b338a81c2da23439704a73c4651e8c4bb",
|
||||
"data/clock_motion.png": "f029226b28b642e80113d86622e9b215ee067a0966feaf5e60604a1e05733955",
|
||||
"data/coffee.png": "cc02f8ca188b167c775a7101b5d767d1e71792cf762c33d6fa15a4599b5a8de7",
|
||||
"data/coins.png": "f8d773fc9cfa6f4d8e5942dc34d0a0788fcaed2a4fefbbed0aef5398d7ef4cba",
|
||||
"data/color.png": "7d2df993de2b4fa2a78e04e5df8050f49a9c511aa75e59ab3bd56ac9c98aef7e",
|
||||
"data/eagle.png": "928f1bbe7403b533265f56db3a6b07c835dfa8e2513f5c5075ca2f1960f6179e",
|
||||
"data/horse.png": "c7fb60789fe394c485f842291ea3b21e50d140f39d6dcb5fb9917cc178225455",
|
||||
"data/grass.png": "b6b6022426b38936c43a4ac09635cd78af074e90f42ffa8227ac8b7452d39f89",
|
||||
"data/hubble_deep_field.jpg": "3a19c5dd8a927a9334bb1229a6d63711b1c0c767fb27e2286e7c84a3e2c2f5f4",
|
||||
"data/ihc.png": "f8dd1aa387ddd1f49d8ad13b50921b237df8e9b262606d258770687b0ef93cef",
|
||||
"data/logo.png": "f2c57fe8af089f08b5ba523d95573c26e62904ac5967f4c8851b27d033690168",
|
||||
"data/lfw_subset.npy": "9560ec2f5edfac01973f63a8a99d00053fecd11e21877e18038fbe500f8e872c",
|
||||
"data/microaneurysms.png": "a1e1be59aa447f8ce082f7fa809997ab369a2b137cb6c4202abc647c7ccf6456",
|
||||
"data/moon.png": "78739619d11f7eb9c165bb5d2efd4772cee557812ec847532dbb1d92ef71f577",
|
||||
"data/motorcycle_left.png": "db18e9c4157617403c3537a6ba355dfeafe9a7eabb6b9b94cb33f6525dd49179",
|
||||
"data/motorcycle_right.png": "5fc913ae870e42a4b662314bc904d1786bcad8e2f0b9b67dba5a229406357797",
|
||||
"data/motorcycle_disp.npz": "2e49c8cebff3fa20359a0cc6880c82e1c03bbb106da81a177218281bc2f113d7",
|
||||
"data/mssim_matlab_output.npz": "cc11a14bfa040c75b02db32282439f2e2e3e96779196c171498afaa70528ed7a",
|
||||
"data/page.png": "341a6f0a61557662b02734a9b6e56ec33a915b2c41886b97509dedf2a43b47a3",
|
||||
"data/phantom.png": "552ff698167aa402cceb17981130607a228a0a0aa7c519299eaa4d5f301ba36c",
|
||||
"data/retina.jpg": "38a07f36f27f095e818aea7b96d34202c05176d30253c66733f2e00379e9e0e6",
|
||||
"data/rocket.jpg": "c2dd0de7c538df8d111e479619b129464d0269d0ae5fd18ca91d33a7fdfea95c",
|
||||
"data/gravel.png": "c48615b451bf1e606fbd72c0aa9f8cc0f068ab7111ef7d93bb9b0f2586440c12",
|
||||
"data/text.png": "bd84aa3a6e3c9887850d45d606c96b2e59433fbef50338570b63c319e668e6d1",
|
||||
"data/chessboard_GRAY_U16.tif": "9fd3392c5b6cbc5f686d8ff83eb57ef91d038ee0852ac26817e5ac99df4c7f45",
|
||||
"data/chessboard_GRAY_U16B.tif": "b0a9270751f0fc340c90b8b615b62b88187b9ab5995942717566735d523cddb2",
|
||||
"data/chessboard_GRAY_U8.npy": "71f394694b721e8a33760a355b3666c9b7d7fc1188ff96b3cd23c2a1d73a38d8",
|
||||
"data/lbpcascade_frontalface_opencv.xml": "03097789a3dcbb0e40d20b9ef82537dbc3b670b6a7f2268d735470f22e003a91",
|
||||
"data/astronaut_GRAY_hog_L1.npy": "5d8ab22b166d1dd49c12caeff9d178ed28132efea3852b952e9d75f7f7f94954",
|
||||
"data/astronaut_GRAY_hog_L2-Hys.npy": "c4dd6e50d1129aada358311cf8880ce8c775f31e0e550fc322c16e43a96d56fe",
|
||||
"data/rank_filter_tests.npz": "efaf5699630f4a53255e91681dc72a965acd4a8aa1f84671c686fb93e7df046d",
|
||||
"data/rank_filters_tests_3d.npz": "1741c2b978424e93558a07d345b2a0d9bfbb33c095c123da147fca066714ab16",
|
||||
"data/multi.fits": "5c71a83436762a52b1925f2f0d83881af7765ed50aede155af2800e54bbd5040",
|
||||
"data/simple.fits": "cd36087fdbb909b6ba506bbff6bcd4c5f4da3a41862608fbac5e8555ef53d40f",
|
||||
"data/palette_color.png": "c4e817035fb9f7730fe95cff1da3866dea01728efc72b6e703d78f7ab9717bdd",
|
||||
"data/palette_gray.png": "bace7f73783bf3ab3b7fdaf701707e4fa09f0dbd0ea72cf5b12ddc73d50b02a9",
|
||||
"data/green_palette.png": "42d49d94be8f9bc76e50639d3701ed0484258721f6b0bd7f50bb1b9274a010f0",
|
||||
"data/truncated.jpg": "4c226038acc78012d335efba29c6119a24444a886842182b7e18db378f4a557d",
|
||||
"data/multipage.tif": "4da0ad0d3df4807a9847247d1b5e565b50d46481f643afb5c37c14802c78130f",
|
||||
"data/multipage_rgb.tif": "1d23b844fd38dce0e2d06f30432817cdb85e52070d8f5460a2ba58aebf34a0de",
|
||||
"data/no_time_for_that_tiny.gif": "20abe94ba9e45f18de416c5fbef8d1f57a499600be40f9a200fae246010eefce",
|
||||
"data/foo3x5x4indexed.png": "48a64c25c6da000ffdb5fcc34ebafe9ba3b1c9b61d7984ea7ca6dc54f9312dfa",
|
||||
"data/mssim_matlab_output.npz": "cc11a14bfa040c75b02db32282439f2e2e3e96779196c171498afaa70528ed7a",
|
||||
"data/gray_morph_output.npz": "49a0dae607cd8d31e134b4bfcbf0d86b13751fdce9667d8bf1ade93d435191b1",
|
||||
"data/disk-matlab-output.npz": "8a39d5c866f6216d6a9c9166312aa4bbf4d18fab3d0dcd963c024985bde5856b",
|
||||
"data/diamond-matlab-output.npz": "02fca68907e2b252b501dfe977eef71ae39fadaaa3702ebdc855195422ae1cc2",
|
||||
"data/bw_text.png": "308c2b09f8975a69b212e103b18520e8cbb7a4eccfce0f757836cd371f1b9094",
|
||||
"data/bw_text_skeleton.npy": "9ff4fc23c6a01497d7987f14e3a97cbcc39cce54b2b3b7ee33b84c1b661d0ae1",
|
||||
"data/_blobs_3d_fiji_skeleton.tif": "e3449ad9819425959952050c147278555e5ffe1c2c4a30df29f6a1f9023e10c3",
|
||||
"data/checker_bilevel.png": "2e207e486545874a2a3e69ba653b28fdef923157be9017559540e65d1bcb8e28",
|
||||
"restoration/tests/astronaut_rl.npy": "3f8373e2c6182a89366e51cef6624e3625deac75fdda1079cbdad2a33322152c",
|
||||
"restoration/tests/camera_rl.npy": "fd4f59af84dd471fbbe79ee70c1b7e68a69864c461f0db5ac587e7975363f78f",
|
||||
"restoration/tests/camera_unsup.npy": "3de10a0b97267352b18886b25d66a967f9e1d78ada61050577d78586cab82baa",
|
||||
"restoration/tests/camera_unsup2.npy": "29cdc60605eb528c5f014baa8564d7d1ba0bd4b3170a66522058cbe5aed0960b",
|
||||
"restoration/tests/camera_wiener.npy": "4505ea8b0d63d03250c6d756560d615751b76dd6ffc4a95972fa260c0c84633e",
|
||||
"registration/tests/data/OriginalX-130Y130.png": "bf24a06d99ae131c97e582ef5e1cd0c648a8dad0caab31281f3564045492811f",
|
||||
"registration/tests/data/OriginalX130Y130.png": "7fdd4c06d504fec35ee0703bd7ed2c08830b075a74c8506bae4a70d682f5a2db",
|
||||
"registration/tests/data/OriginalX75Y75.png": "c5cd58893c93140df02896df80b13ecf432f5c86eeaaf8fb311aec52a65c7016",
|
||||
"registration/tests/data/TransformedX-130Y130.png": "1cda90ed69c921eb7605b73b76d141cf4ea03fb8ce3336445ca08080e40d7375",
|
||||
"registration/tests/data/TransformedX130Y130.png": "bb10c6ae3f91a313b0ac543efdb7ca69c4b95e55674c65a88472a6c4f4692a25",
|
||||
"registration/tests/data/TransformedX75Y75.png": "a1e9ead5f8e4a0f604271e1f9c50e89baf53f068f1d19fab2876af4938e695ea",
|
||||
"data/brain.tiff": "bcdbaf424fbad7b1fb0f855f608c68e5a838f35affc323ff04ea17f678eef5c6",
|
||||
"data/cells3d.tif": "afc7c7d80d38bfde09788b4064ac1e64ec14e88454ab785ebdc8dbba5ca3b222",
|
||||
"data/palisades_of_vogt.tif": "7f205b626407e194974cead67c4d3909344cf59c42d229a349da0198183e5bd0",
|
||||
"data/kidney.tif": "80c0799bc58b08cf6eaa53ecd202305eb42fd7bc73746cb6c5064dbeae7e8476",
|
||||
"data/lily.tif": "395c2f0194c25b9824a8cd79266920362a0816bc9e906dd392adce2d8309af03",
|
||||
"data/mitosis.tif": "2751ba667c4067c5d30817cff004aa06f6f6287f1cdbb5b8c9c6a500308cb456",
|
||||
"data/skin.jpg": "8759fe080509712163453f4b17106582b8513e73b0788d80160abf840e272075",
|
||||
"data/pivchallenge-B-B001_1.tif": "e95e09abbcecba723df283ac7d361766328abd943701a2ec2f345d4a2014da2a",
|
||||
"data/pivchallenge-B-B001_2.tif": "4ceb5407e4e333476a0f264c14b7a3f6c0e753fcdc99ee1c4b8196e5f823805e",
|
||||
"data/protein_transport.tif": "a8e24e8d187f33e92ee28508d5615286c850ca75374af7e74e527d290e8b06ea",
|
||||
"data/solidification.tif": "50ef9a52c621b7c0c506ad1fe1b8ee8a158a4d7c8e50ddfce1e273a422dca3f9",
|
||||
}
|
||||
|
||||
registry_urls = {
|
||||
"data/brain.tiff": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/brain.tiff",
|
||||
"data/cells3d.tif": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/cells3d.tif",
|
||||
"data/palisades_of_vogt.tif": "https://gitlab.com/scikit-image/data/-/raw/b2bc880f3bac23a583724befe8388dae368c52fe/in-vivo-cornea-spots.tif",
|
||||
"data/eagle.png": "https://gitlab.com/scikit-image/data/-/raw/1e4f62ac31ba4553d176d4473a5967ad1b076d62/eagle.png",
|
||||
"data/kidney.tif": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/kidney-tissue-fluorescence.tif",
|
||||
"data/lily.tif": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/lily-of-the-valley-fluorescence.tif",
|
||||
"data/mitosis.tif": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/AS_09125_050116030001_D03f00d0.tif",
|
||||
"data/rank_filters_tests_3d.npz": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/Tests_besides_Equalize_Otsu/add18_entropy/rank_filters_tests_3d.npz",
|
||||
"data/skin.jpg": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/Normal_Epidermis_and_Dermis_with_Intradermal_Nevus_10x.JPG",
|
||||
"data/pivchallenge-B-B001_1.tif": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/pivchallenge/B/B001_1.tif",
|
||||
"data/pivchallenge-B-B001_2.tif": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/pivchallenge/B/B001_2.tif",
|
||||
"data/protein_transport.tif": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/NPCsingleNucleus.tif",
|
||||
"data/solidification.tif": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/nickel_solidification.tif",
|
||||
"restoration/tests/astronaut_rl.npy": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/astronaut_rl.npy",
|
||||
"data/gray_morph_output.npz": "https://gitlab.com/scikit-image/data/-/raw/806548e112bcf2b708a9a32275d335cb592480fd/Tests_besides_Equalize_Otsu/gray_morph_output.npz",
|
||||
}
|
||||
|
||||
legacy_registry = {
|
||||
('data/' + filename): registry['data/' + filename] for filename in legacy_datasets
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 773 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user