Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7430c41
Complete deprecation of selem parameter
lagru Oct 16, 2022
d951774
Complete deprecation of selem module
lagru Oct 16, 2022
64e0023
Complete deprecation of in_place parameter
lagru Oct 16, 2022
198566f
Complete deprecation of max_iterations parameter
lagru Oct 16, 2022
6db637d
Complete deprecation of max_iter parameter
lagru Oct 16, 2022
50cdfb0
Complete deprecation of iterations parameter
lagru Oct 16, 2022
4cb8866
Complete deprecation of n_iter_max parameter
lagru Oct 16, 2022
56f0431
Complete deprecation of compute_hessian_eigenvalues
lagru Oct 16, 2022
f77d0da
Complete deprecation of input parameter
lagru Oct 16, 2022
171d34a
Complete deprecation of greyco* functions
lagru Oct 16, 2022
9b2c8f0
Complete deprecation of multichannel kwarg
lagru Oct 18, 2022
c7337b6
Remove unused deprecate_multichannel_kwarg
lagru Oct 18, 2022
c25941e
Complete deprecation of height, width in rectangle
lagru Oct 19, 2022
1beab4a
Remove completed items in TODO.txt
lagru Oct 19, 2022
6a24f27
Complete deprecation of neighbourhood parameter
lagru Oct 19, 2022
b53b46a
Remove deprecated grey and greyreconstruct modules
lagru Oct 19, 2022
38c23c4
Remove warning about deprecated *_iter
lagru Oct 19, 2022
998ee00
Revert "Complete deprecation of compute_hessian_eigenvalues"
lagru Oct 25, 2022
a5421d5
Deprecate automatic channel detection in gaussian
lagru Oct 26, 2022
d752adf
Complete deprecation of coordinates parameter
lagru Oct 29, 2022
206e599
Remove outdated TODO for CircleModel.estimate
lagru Oct 29, 2022
bc476b7
Merge branch 'main' into complete-0.20-deprecations
lagru Oct 29, 2022
9b70ed6
Remove removed files from meson.build
lagru Oct 29, 2022
e7d8c75
Remove obsolete tests for coordinates parameter
lagru Oct 29, 2022
15638d4
Catch expected warning for deprecated color channel inference in gaus…
grlee77 Nov 18, 2022
cc32ae2
fix typo in prior commit
grlee77 Nov 18, 2022
dc843fe
Merge remote-tracking branch 'upstream/main' into complete-0.20-depre…
grlee77 Nov 18, 2022
246bc46
Update skimage/_shared/filters.py
lagru Nov 23, 2022
529c74a
Explain proxy value in more clearly
lagru Nov 23, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 8 additions & 12 deletions TODO.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,16 @@ Version 0.17
* Finalize ``skimage.future.graph`` API.
* Finalize ``skimage.future.manual_segmentation`` API.

Version 1.0
-----------

* consider removing the argument `coordinates` in
`skimage.segmentation.active_contour`, which has not effect.
* In ``skimage/morphology/misc.py`` remove the deprecated parameter
``in_place`` in remove_small_holes(), remove_small_objects() and
clear_border(), and update the tests.
* Remove the deprecation warning for `input` kwarg of the `label`
function in `skimage/measure/_label.py`
* Change the warning about poorly conditioned data to a ValueError within
`skimage.measure.CircleModel.estimate` and update the tests in `test_circle_model_insufficient_data` accordingly.
Version 0.21
------------
* In ``skimage/filters/lpi_filter.py``, remove the deprecated function
inverse().
* Set `channel_axis=None` in `skimage._shared.filters.gaussian` and remove
`skimage._shared.filters._PatchClassRepr`,
`skimage/_shared/filters.ChannelAxisNotSet`, and
`skimage.filters.tests.test_gaussian.test_deprecated_automatic_channel_detection`.
Also remove the associated warning in `skimage._shared.filters.gaussian`'s
docstring.

Other (2022)
------------
Expand Down
8 changes: 4 additions & 4 deletions benchmarks/benchmark_rank.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ class RankSuite:

def setup(self, filter_func, shape):
self.image = np.random.randint(0, 255, size=shape, dtype=np.uint8)
self.selem = disk(1)
self.footprint = disk(1)

def time_filter(self, filter_func, shape):
getattr(rank, filter_func)(self.image, self.selem)
getattr(rank, filter_func)(self.image, self.footprint)


class Rank3DSuite:
Expand All @@ -25,7 +25,7 @@ class Rank3DSuite:

def setup(self, filter3d, shape3d):
self.volume = np.random.randint(0, 255, size=shape3d, dtype=np.uint8)
self.selem_3d = ball(1)
self.footprint_3d = ball(1)

def time_3d_filters(self, filter3d, shape3d):
getattr(rank, filter3d)(self.volume, self.selem_3d)
getattr(rank, filter3d)(self.volume, self.footprint_3d)
63 changes: 46 additions & 17 deletions skimage/_shared/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,32 @@
import numpy as np
from scipy import ndimage as ndi

from .._shared import utils
from .._shared.utils import _supported_float_type, convert_to_float, warn


@utils.deprecate_multichannel_kwarg(multichannel_position=5)
class _PatchClassRepr(type):
"""Control class representations in rendered signatures."""
def __repr__(cls):
return f"<{cls.__name__}>"


class ChannelAxisNotSet(metaclass=_PatchClassRepr):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the metaclass necessary?

Copy link
Copy Markdown
Member Author

@lagru lagru Nov 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure there are other ways to control how this signal object is rendered in the signature (I would post the link but strangely the pipeline did not build the docs?). I initially included the __repr__ in this class itself which requires creating an instance of it in the same scope to check for identity in the function body. Compared to that, I actually found the metaclass-based approach clearer and easier to document. The class itself clearly is the signal and there is no confusion about a second symbol that also looks like a signal; it's clearly a metaclass only responsible for modifying the representation.

It's not something I feel strongly about, though. The verbose answer is mainly because I'm rationalizing my gut feeling about which approach "felt better" in retrospective. 😄

"""Signal that the `channel_axis` parameter is not set.

This is a proxy object, used to signal to `skimage.filters.gaussian` that
the `channel_axis` parameter has not been set, in which case the function
will determine whether a color channel is present. We cannot use ``None``
for this purpose as it has its own meaning which indicates that the given
image is grayscale.

This automatic behavior was broken in v0.19, recovered but deprecated in
v0.20 and will be removed in v0.21.
"""


def gaussian(image, sigma=1, output=None, mode='nearest', cval=0,
multichannel=None, preserve_range=False, truncate=4.0, *,
channel_axis=None):
preserve_range=False, truncate=4.0, *,
channel_axis=ChannelAxisNotSet):
"""Multi-dimensional Gaussian filter.

Parameters
Expand All @@ -38,13 +56,6 @@ def gaussian(image, sigma=1, output=None, mode='nearest', cval=0,
cval : scalar, optional
Value to fill past edges of input if ``mode`` is 'constant'. Default
is 0.0
multichannel : bool, optional (default: None)
Whether the last axis of the image is to be interpreted as multiple
channels. If True, each channel is filtered separately (channels are
not mixed together). Only 3 channels are supported. If ``None``,
the function will attempt to guess this, and raise a warning if
ambiguous, when the array has shape (M, N, 3).
This argument is deprecated: specify `channel_axis` instead.
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``
Expand All @@ -63,6 +74,16 @@ def gaussian(image, sigma=1, output=None, mode='nearest', cval=0,
.. versionadded:: 0.19
``channel_axis`` was added in 0.19.

.. warning::

Automatic detection of the color channel based on the old deprecated
`multichannel=None` was broken in version 0.19. In 0.20 this
behavior is fixed. The last axis of an `image` with dimensions
(M, N, 3) is interpreted as a color channel if `channel_axis` is not
set by the user (signaled by the default proxy value
`ChannelAxisNotSet`). Starting with 0.21, `channel_axis=None` will
be used as the new default value.

Returns
-------
filtered_image : ndarray
Expand Down Expand Up @@ -113,12 +134,20 @@ def gaussian(image, sigma=1, output=None, mode='nearest', cval=0,
>>> filtered_img = gaussian(image, sigma=1, channel_axis=-1)

"""
if image.ndim == 3 and image.shape[-1] == 3 and channel_axis is None:
msg = ("Images with dimensions (M, N, 3) are interpreted as 2D+RGB "
"by default. Use `multichannel=False` to interpret as "
"3D image with last dimension of length 3.")
warn(RuntimeWarning(msg))
channel_axis = -1
if channel_axis is ChannelAxisNotSet:
if image.ndim == 3 and image.shape[-1] == 3:
warn(
"Automatic detection of the color channel was deprecated in "
"v0.19, and `channel_axis=None` will be the new default in "
"v0.21. Set `channel_axis=-1` explicitly to silence this "
"warning.",
FutureWarning,
stacklevel=2,
)
channel_axis = -1
else:
channel_axis = None

if np.any(np.asarray(sigma) < 0.0):
raise ValueError("Sigma values less than zero are not valid")
if channel_axis is not None:
Expand Down
24 changes: 8 additions & 16 deletions skimage/_shared/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
change_default_value, remove_arg,
_supported_float_type,
channel_as_last_axis)
from skimage.feature import hog
from skimage.transform import pyramid_gaussian

complex_dtypes = [np.complex64, np.complex128]
if hasattr(np, 'complex256'):
Expand Down Expand Up @@ -280,23 +278,17 @@ def test_decorated_channel_axis_shape(channel_axis):
assert size == x.shape[channel_axis]


def test_decorator_warnings():
"""Assert that warning message issued by decorator points to
expected file and line number.
"""

with pytest.warns(FutureWarning) as record:
pyramid_gaussian(None, multichannel=True)
expected_lineno = inspect.currentframe().f_lineno - 1

assert record[0].lineno == expected_lineno
assert record[0].filename == __file__
@deprecate_kwarg({"old_kwarg": "new_kwarg"}, deprecated_version="x.y.z")
def _function_with_deprecated_kwarg(*, new_kwarg):
pass

img = np.random.rand(100, 100, 3)

def test_deprecate_kwarg_location():
"""Assert that warning message issued by deprecate_kwarg points to
file and line number where decorated function is called.
"""
with pytest.warns(FutureWarning) as record:
hog(img, multichannel=True)
_function_with_deprecated_kwarg(old_kwarg=True)
expected_lineno = inspect.currentframe().f_lineno - 1

assert record[0].lineno == expected_lineno
assert record[0].filename == __file__
66 changes: 0 additions & 66 deletions skimage/_shared/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,72 +288,6 @@ def fixed_func(*args, **kwargs):
return fixed_func


class deprecate_multichannel_kwarg(deprecate_kwarg):
"""Decorator for deprecating multichannel keyword in favor of channel_axis.

Parameters
----------
removed_version : str
The package version in which the deprecated argument will be
removed.

"""

def __init__(self, removed_version='1.0', multichannel_position=None):
super().__init__(
kwarg_mapping={'multichannel': 'channel_axis'},
deprecated_version='0.19',
warning_msg=None,
removed_version=removed_version)
self.position = multichannel_position

def __call__(self, func):

stack_rank = _get_stack_rank(func)

@functools.wraps(func)
def fixed_func(*args, **kwargs):
stacklevel = 1 + self.get_stack_length(func) - stack_rank

if self.position is not None and len(args) > self.position:
warning_msg = (
"Providing the `multichannel` argument positionally to "
"{func_name} is deprecated. Use the `channel_axis` kwarg "
"instead."
)
warnings.warn(warning_msg.format(func_name=func.__name__),
FutureWarning,
stacklevel=stacklevel)
if 'channel_axis' in kwargs:
raise ValueError(
"Cannot provide both a `channel_axis` kwarg and a "
"positional `multichannel` value."
)
else:
channel_axis = -1 if args[self.position] else None
kwargs['channel_axis'] = channel_axis

if 'multichannel' in kwargs:
# warn that the function interface has changed:
warnings.warn(self.warning_msg.format(
old_arg='multichannel', func_name=func.__name__,
new_arg='channel_axis'), FutureWarning,
stacklevel=stacklevel)

# multichannel = True -> last axis corresponds to channels
convert = {True: -1, False: None}
kwargs['channel_axis'] = convert[kwargs.pop('multichannel')]

# Call the function with the fixed arguments
return func(*args, **kwargs)

if func.__doc__ is not None:
newdoc = docstring_add_deprecated(
func, {'multichannel': 'channel_axis'}, '0.19')
fixed_func.__doc__ = newdoc
return fixed_func


class channel_as_last_axis:
"""Decorator for automatically making channels axis last for all arrays.

Expand Down
8 changes: 1 addition & 7 deletions skimage/draw/_random_shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from . import (polygon as draw_polygon, disk as draw_disk,
ellipse as draw_ellipse)
from .._shared.utils import deprecate_multichannel_kwarg, warn
from .._shared.utils import warn


def _generate_rectangle_mask(point, image, shape, random):
Expand Down Expand Up @@ -291,13 +291,11 @@ def _generate_random_colors(num_colors, num_channels, intensity_range, random):
return np.transpose(colors)


@deprecate_multichannel_kwarg(multichannel_position=5)
def random_shapes(image_shape,
max_shapes,
min_shapes=1,
min_size=2,
max_size=None,
multichannel=True,
num_channels=3,
shape=None,
intensity_range=None,
Expand Down Expand Up @@ -330,10 +328,6 @@ def random_shapes(image_shape,
The minimum dimension of each shape to fit into the image.
max_size : int, optional
The maximum dimension of each shape to fit into the image.
multichannel : bool, optional
If True, the generated image has ``num_channels`` color channels,
otherwise generates grayscale image. This argument is deprecated:
specify `channel_axis` instead.
num_channels : int, optional
Number of channels in the generated image. If 1, generate monochrome
images, else color images with multiple channels. Ignored if
Expand Down
15 changes: 4 additions & 11 deletions skimage/draw/tests/test_random_shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,8 @@ def test_generates_gray_images_with_correct_shape():


def test_generates_gray_images_with_correct_shape_deprecated_multichannel():
with expected_warnings(["`multichannel` is a deprecated argument"]):
image, _ = random_shapes(
(4567, 123), min_shapes=3, max_shapes=20, multichannel=False)
assert image.shape == (4567, 123)

# repeat prior test, but check for positional multichannel warning
with expected_warnings(["Providing the `multichannel` argument"]):
image, _ = random_shapes((4567, 123), 20, 3, 2, None, False)
image, _ = random_shapes(
(4567, 123), min_shapes=3, max_shapes=20, channel_axis=None)
assert image.shape == (4567, 123)


Expand Down Expand Up @@ -167,9 +161,8 @@ def test_can_generate_one_by_one_rectangle():

def test_throws_when_intensity_range_out_of_range():
with testing.raises(ValueError):
with expected_warnings(["`multichannel` is a deprecated argument"]):
random_shapes((1000, 1234), max_shapes=1, multichannel=False,
intensity_range=(0, 256))
random_shapes((1000, 1234), max_shapes=1, channel_axis=None,
intensity_range=(0, 256))
with testing.raises(ValueError):
random_shapes((2, 2), max_shapes=1,
intensity_range=((-1, 255),))
Expand Down
7 changes: 1 addition & 6 deletions skimage/exposure/histogram_matching.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ def _match_cumulative_cdf(source, template):


@utils.channel_as_last_axis(channel_arg_positions=(0, 1))
@utils.deprecate_multichannel_kwarg()
def match_histograms(image, reference, *, channel_axis=None,
multichannel=False):
def match_histograms(image, reference, *, channel_axis=None):
"""Adjust an image so that its cumulative histogram matches that of another.

The adjustment is applied separately for each channel.
Expand All @@ -50,9 +48,6 @@ def match_histograms(image, reference, *, channel_axis=None,
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.
multichannel : bool, optional
Apply the matching separately for each channel. This argument is
deprecated: specify `channel_axis` instead.

Returns
-------
Expand Down
14 changes: 6 additions & 8 deletions skimage/exposure/tests/test_histogram_matching.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from skimage import data
from skimage import exposure
from skimage._shared.testing import expected_warnings
from skimage._shared.utils import _supported_float_type
from skimage.exposure import histogram_matching

Expand All @@ -26,17 +25,16 @@ class TestMatchHistogram:
image_rgb = data.chelsea()
template_rgb = data.astronaut()

@pytest.mark.parametrize('image, reference, multichannel', [
(image_rgb, template_rgb, True),
(image_rgb[:, :, 0], template_rgb[:, :, 0], False)
@pytest.mark.parametrize('image, reference, channel_axis', [
(image_rgb, template_rgb, -1),
(image_rgb[:, :, 0], template_rgb[:, :, 0], None)
])
def test_match_histograms(self, image, reference, multichannel):
def test_match_histograms(self, image, reference, channel_axis):
"""Assert that pdf of matched image is close to the reference's pdf for
all channels and all values of matched"""

with expected_warnings(["`multichannel` is a deprecated argument"]):
matched = exposure.match_histograms(image, reference,
multichannel=multichannel)
matched = exposure.match_histograms(image, reference,
channel_axis=channel_axis)

matched_pdf = self._calculate_image_empirical_pdf(matched)
reference_pdf = self._calculate_image_empirical_pdf(reference)
Expand Down
Loading