1import numpy as np
2
3from ..color import rgb2gray, rgba2rgb
4from ..util.dtype import dtype_range, dtype_limits
5from .._shared import utils
6
7
8__all__ = ['histogram', 'cumulative_distribution', 'equalize_hist',
9           'rescale_intensity', 'adjust_gamma', 'adjust_log', 'adjust_sigmoid']
10
11
12DTYPE_RANGE = dtype_range.copy()
13DTYPE_RANGE.update((d.__name__, limits) for d, limits in dtype_range.items())
14DTYPE_RANGE.update({'uint10': (0, 2 ** 10 - 1),
15                    'uint12': (0, 2 ** 12 - 1),
16                    'uint14': (0, 2 ** 14 - 1),
17                    'bool': dtype_range[bool],
18                    'float': dtype_range[np.float64]})
19
20
21def _offset_array(arr, low_boundary, high_boundary):
22    """Offset the array to get the lowest value at 0 if negative."""
23    if low_boundary < 0:
24        offset = low_boundary
25        dyn_range = high_boundary - low_boundary
26        # get smallest dtype that can hold both minimum and offset maximum
27        offset_dtype = np.promote_types(np.min_scalar_type(dyn_range),
28                                        np.min_scalar_type(low_boundary))
29        if arr.dtype != offset_dtype:
30            # prevent overflow errors when offsetting
31            arr = arr.astype(offset_dtype)
32        arr = arr - offset
33    else:
34        offset = 0
35    return arr, offset
36
37
38def _bincount_histogram_centers(image, source_range):
39    """Compute bin centers for bincount-based histogram."""
40    if source_range not in ['image', 'dtype']:
41        raise ValueError(
42            f'Incorrect value for `source_range` argument: {source_range}'
43        )
44    if source_range == 'image':
45        image_min = int(image.min().astype(np.int64))
46        image_max = int(image.max().astype(np.int64))
47    elif source_range == 'dtype':
48        image_min, image_max = dtype_limits(image, clip_negative=False)
49    bin_centers = np.arange(image_min, image_max + 1)
50    return bin_centers
51
52
53def _bincount_histogram(image, source_range, bin_centers=None):
54    """
55    Efficient histogram calculation for an image of integers.
56
57    This function is significantly more efficient than np.histogram but
58    works only on images of integers. It is based on np.bincount.
59
60    Parameters
61    ----------
62    image : array
63        Input image.
64    source_range : string
65        'image' determines the range from the input image.
66        'dtype' determines the range from the expected range of the images
67        of that data type.
68
69    Returns
70    -------
71    hist : array
72        The values of the histogram.
73    bin_centers : array
74        The values at the center of the bins.
75    """
76    if bin_centers is None:
77        bin_centers = _bincount_histogram_centers(image, source_range)
78    image_min, image_max = bin_centers[0], bin_centers[-1]
79    image, offset = _offset_array(image, image_min, image_max)
80    hist = np.bincount(image.ravel(), minlength=image_max - image_min + 1)
81    if source_range == 'image':
82        idx = max(image_min, 0)
83        hist = hist[idx:]
84    return hist, bin_centers
85
86
87def _get_outer_edges(image, hist_range):
88    """Determine the outer bin edges to use for `numpy.histogram`.
89
90    These are obtained from either the image or hist_range.
91
92    Parameters
93    ----------
94    image : ndarray
95        Image for which the histogram is to be computed.
96    hist_range: 2-tuple of int or None
97        Range of values covered by the histogram bins. If None, the minimum
98        and maximum values of `image` are used.
99
100    Returns
101    -------
102    first_edge, last_edge : int
103        The range spanned by the histogram bins.
104
105    Notes
106    -----
107    This function is adapted from ``np.lib.histograms._get_outer_edges``.
108    """
109    if hist_range is not None:
110        first_edge, last_edge = hist_range
111        if first_edge > last_edge:
112            raise ValueError(
113                "max must be larger than min in hist_range parameter."
114            )
115        if not (np.isfinite(first_edge) and np.isfinite(last_edge)):
116            raise ValueError(
117                f'supplied hist_range of [{first_edge}, {last_edge}] is '
118                f'not finite'
119            )
120    elif image.size == 0:
121        # handle empty arrays. Can't determine hist_range, so use 0-1.
122        first_edge, last_edge = 0, 1
123    else:
124        first_edge, last_edge = image.min(), image.max()
125        if not (np.isfinite(first_edge) and np.isfinite(last_edge)):
126            raise ValueError(
127                f'autodetected hist_range of [{first_edge}, {last_edge}] is '
128                f'not finite'
129            )
130
131    # expand empty hist_range to avoid divide by zero
132    if first_edge == last_edge:
133        first_edge = first_edge - 0.5
134        last_edge = last_edge + 0.5
135
136    return first_edge, last_edge
137
138
139def _get_bin_edges(image, nbins, hist_range):
140    """Computes histogram bins for use with `numpy.histogram`.
141
142    Parameters
143    ----------
144    image : ndarray
145        Image for which the histogram is to be computed.
146    nbins : int
147        The number of bins.
148    hist_range: 2-tuple of int
149        Range of values covered by the histogram bins.
150
151    Returns
152    -------
153    bin_edges : ndarray
154        The histogram bin edges.
155
156    Notes
157    -----
158    This function is a simplified version of
159    ``np.lib.histograms._get_bin_edges`` that only supports uniform bins.
160    """
161    first_edge, last_edge = _get_outer_edges(image, hist_range)
162    # numpy/gh-10322 means that type resolution rules are dependent on array
163    # shapes. To avoid this causing problems, we pick a type now and stick
164    # with it throughout.
165    bin_type = np.result_type(first_edge, last_edge, image)
166    if np.issubdtype(bin_type, np.integer):
167        bin_type = np.result_type(bin_type, float)
168
169    # compute bin edges
170    bin_edges = np.linspace(
171        first_edge, last_edge, nbins + 1, endpoint=True, dtype=bin_type
172    )
173    return bin_edges
174
175
176def _get_numpy_hist_range(image, source_range):
177    if source_range == 'image':
178        hist_range = None
179    elif source_range == 'dtype':
180        hist_range = dtype_limits(image, clip_negative=False)
181    else:
182        ValueError('Wrong value for the `source_range` argument')
183    return hist_range
184
185
186@utils.channel_as_last_axis(multichannel_output=False)
187def histogram(image, nbins=256, source_range='image', normalize=False, *,
188              channel_axis=None):
189    """Return histogram of image.
190
191    Unlike `numpy.histogram`, this function returns the centers of bins and
192    does not rebin integer arrays. For integer arrays, each integer value has
193    its own bin, which improves speed and intensity-resolution.
194
195    If `channel_axis` is not set, the histogram is computed on the flattened
196    image. For color or multichannel images, set ``channel_axis`` to use a
197    common binning for all channels. Alternatively, one may apply the function
198    separately on each channel to obtain a histogram for each color channel
199    with separate binning.
200
201    Parameters
202    ----------
203    image : array
204        Input image.
205    nbins : int, optional
206        Number of bins used to calculate histogram. This value is ignored for
207        integer arrays.
208    source_range : string, optional
209        'image' (default) determines the range from the input image.
210        'dtype' determines the range from the expected range of the images
211        of that data type.
212    normalize : bool, optional
213        If True, normalize the histogram by the sum of its values.
214    channel_axis : int or None, optional
215        If None, the image is assumed to be a grayscale (single channel) image.
216        Otherwise, this parameter indicates which axis of the array corresponds
217        to channels.
218
219    Returns
220    -------
221    hist : array
222        The values of the histogram. When ``channel_axis`` is not None, hist
223        will be a 2D array where the first axis corresponds to channels.
224    bin_centers : array
225        The values at the center of the bins.
226
227    See Also
228    --------
229    cumulative_distribution
230
231    Examples
232    --------
233    >>> from skimage import data, exposure, img_as_float
234    >>> image = img_as_float(data.camera())
235    >>> np.histogram(image, bins=2)
236    (array([ 93585, 168559]), array([0. , 0.5, 1. ]))
237    >>> exposure.histogram(image, nbins=2)
238    (array([ 93585, 168559]), array([0.25, 0.75]))
239    """
240    sh = image.shape
241    if len(sh) == 3 and sh[-1] < 4 and channel_axis is None:
242        utils.warn('This might be a color image. The histogram will be '
243                   'computed on the flattened image. You can instead '
244                   'apply this function to each color channel, or set '
245                   'channel_axis.')
246
247    if channel_axis is not None:
248        channels = sh[-1]
249        hist = []
250
251        # compute bins based on the raveled array
252        if np.issubdtype(image.dtype, np.integer):
253            # here bins corresponds to the bin centers
254            bins = _bincount_histogram_centers(image, source_range)
255        else:
256            # determine the bin edges for np.histogram
257            hist_range = _get_numpy_hist_range(image, source_range)
258            bins = _get_bin_edges(image, nbins, hist_range)
259
260        for chan in range(channels):
261            h, bc = _histogram(image[..., chan], bins, source_range, normalize)
262            hist.append(h)
263        # Convert to numpy arrays
264        bin_centers = np.asarray(bc)
265        hist = np.stack(hist, axis=0)
266    else:
267        hist, bin_centers = _histogram(image, nbins, source_range, normalize)
268
269    return hist, bin_centers
270
271
272def _histogram(image, bins, source_range, normalize):
273    """
274
275    Parameters
276    ----------
277    image : ndarray
278        Image for which the histogram is to be computed.
279    bins : int or ndarray
280        The number of histogram bins. For images with integer dtype, an array
281        containing the bin centers can also be provided. For images with
282        floating point dtype, this can be an array of bin_edges for use by
283        ``np.histogram``.
284    source_range : string, optional
285        'image' (default) determines the range from the input image.
286        'dtype' determines the range from the expected range of the images
287        of that data type.
288    normalize : bool, optional
289        If True, normalize the histogram by the sum of its values.
290    """
291
292    image = image.flatten()
293    # For integer types, histogramming with bincount is more efficient.
294    if np.issubdtype(image.dtype, np.integer):
295        bin_centers = bins if isinstance(bins, np.ndarray) else None
296        hist, bin_centers = _bincount_histogram(
297            image, source_range, bin_centers
298        )
299    else:
300        hist_range = _get_numpy_hist_range(image, source_range)
301        hist, bin_edges = np.histogram(image, bins=bins, range=hist_range)
302        bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2.
303
304    if normalize:
305        hist = hist / np.sum(hist)
306    return hist, bin_centers
307
308
309def cumulative_distribution(image, nbins=256):
310    """Return cumulative distribution function (cdf) for the given image.
311
312    Parameters
313    ----------
314    image : array
315        Image array.
316    nbins : int, optional
317        Number of bins for image histogram.
318
319    Returns
320    -------
321    img_cdf : array
322        Values of cumulative distribution function.
323    bin_centers : array
324        Centers of bins.
325
326    See Also
327    --------
328    histogram
329
330    References
331    ----------
332    .. [1] https://en.wikipedia.org/wiki/Cumulative_distribution_function
333
334    Examples
335    --------
336    >>> from skimage import data, exposure, img_as_float
337    >>> image = img_as_float(data.camera())
338    >>> hi = exposure.histogram(image)
339    >>> cdf = exposure.cumulative_distribution(image)
340    >>> np.alltrue(cdf[0] == np.cumsum(hi[0])/float(image.size))
341    True
342    """
343    hist, bin_centers = histogram(image, nbins)
344    img_cdf = hist.cumsum()
345    img_cdf = img_cdf / float(img_cdf[-1])
346
347    # cast img_cdf to single precision for float32 or float16 inputs
348    cdf_dtype = utils._supported_float_type(image.dtype)
349    img_cdf = img_cdf.astype(cdf_dtype, copy=False)
350
351    return img_cdf, bin_centers
352
353
354def equalize_hist(image, nbins=256, mask=None):
355    """Return image after histogram equalization.
356
357    Parameters
358    ----------
359    image : array
360        Image array.
361    nbins : int, optional
362        Number of bins for image histogram. Note: this argument is
363        ignored for integer images, for which each integer is its own
364        bin.
365    mask : ndarray of bools or 0s and 1s, optional
366        Array of same shape as `image`. Only points at which mask == True
367        are used for the equalization, which is applied to the whole image.
368
369    Returns
370    -------
371    out : float array
372        Image array after histogram equalization.
373
374    Notes
375    -----
376    This function is adapted from [1]_ with the author's permission.
377
378    References
379    ----------
380    .. [1] http://www.janeriksolem.net/histogram-equalization-with-python-and.html
381    .. [2] https://en.wikipedia.org/wiki/Histogram_equalization
382
383    """
384    if mask is not None:
385        mask = np.array(mask, dtype=bool)
386        cdf, bin_centers = cumulative_distribution(image[mask], nbins)
387    else:
388        cdf, bin_centers = cumulative_distribution(image, nbins)
389    out = np.interp(image.flat, bin_centers, cdf)
390    out = out.reshape(image.shape)
391    # Unfortunately, np.interp currently always promotes to float64, so we
392    # have to cast back to single precision when float32 output is desired
393    return out.astype(utils._supported_float_type(image.dtype), copy=False)
394
395
396def intensity_range(image, range_values='image', clip_negative=False):
397    """Return image intensity range (min, max) based on desired value type.
398
399    Parameters
400    ----------
401    image : array
402        Input image.
403    range_values : str or 2-tuple, optional
404        The image intensity range is configured by this parameter.
405        The possible values for this parameter are enumerated below.
406
407        'image'
408            Return image min/max as the range.
409        'dtype'
410            Return min/max of the image's dtype as the range.
411        dtype-name
412            Return intensity range based on desired `dtype`. Must be valid key
413            in `DTYPE_RANGE`. Note: `image` is ignored for this range type.
414        2-tuple
415            Return `range_values` as min/max intensities. Note that there's no
416            reason to use this function if you just want to specify the
417            intensity range explicitly. This option is included for functions
418            that use `intensity_range` to support all desired range types.
419
420    clip_negative : bool, optional
421        If True, clip the negative range (i.e. return 0 for min intensity)
422        even if the image dtype allows negative values.
423    """
424    if range_values == 'dtype':
425        range_values = image.dtype.type
426
427    if range_values == 'image':
428        i_min = np.min(image)
429        i_max = np.max(image)
430    elif range_values in DTYPE_RANGE:
431        i_min, i_max = DTYPE_RANGE[range_values]
432        if clip_negative:
433            i_min = 0
434    else:
435        i_min, i_max = range_values
436    return i_min, i_max
437
438
439def _output_dtype(dtype_or_range, image_dtype):
440    """Determine the output dtype for rescale_intensity.
441
442    The dtype is determined according to the following rules:
443    - if ``dtype_or_range`` is a dtype, that is the output dtype.
444    - if ``dtype_or_range`` is a dtype string, that is the dtype used, unless
445      it is not a NumPy data type (e.g. 'uint12' for 12-bit unsigned integers),
446      in which case the data type that can contain it will be used
447      (e.g. uint16 in this case).
448    - if ``dtype_or_range`` is a pair of values, the output data type will be
449      ``_supported_float_type(image_dtype)``. This preserves float32 output for
450      float32 inputs.
451
452    Parameters
453    ----------
454    dtype_or_range : type, string, or 2-tuple of int/float
455        The desired range for the output, expressed as either a NumPy dtype or
456        as a (min, max) pair of numbers.
457    image_dtype : np.dtype
458        The input image dtype.
459
460    Returns
461    -------
462    out_dtype : type
463        The data type appropriate for the desired output.
464    """
465    if type(dtype_or_range) in [list, tuple, np.ndarray]:
466        # pair of values: always return float.
467        return utils._supported_float_type(image_dtype)
468    if type(dtype_or_range) == type:
469        # already a type: return it
470        return dtype_or_range
471    if dtype_or_range in DTYPE_RANGE:
472        # string key in DTYPE_RANGE dictionary
473        try:
474            # if it's a canonical numpy dtype, convert
475            return np.dtype(dtype_or_range).type
476        except TypeError:  # uint10, uint12, uint14
477            # otherwise, return uint16
478            return np.uint16
479    else:
480        raise ValueError(
481            'Incorrect value for out_range, should be a valid image data '
482            f'type or a pair of values, got {dtype_or_range}.'
483        )
484
485
486def rescale_intensity(image, in_range='image', out_range='dtype'):
487    """Return image after stretching or shrinking its intensity levels.
488
489    The desired intensity range of the input and output, `in_range` and
490    `out_range` respectively, are used to stretch or shrink the intensity range
491    of the input image. See examples below.
492
493    Parameters
494    ----------
495    image : array
496        Image array.
497    in_range, out_range : str or 2-tuple, optional
498        Min and max intensity values of input and output image.
499        The possible values for this parameter are enumerated below.
500
501        'image'
502            Use image min/max as the intensity range.
503        'dtype'
504            Use min/max of the image's dtype as the intensity range.
505        dtype-name
506            Use intensity range based on desired `dtype`. Must be valid key
507            in `DTYPE_RANGE`.
508        2-tuple
509            Use `range_values` as explicit min/max intensities.
510
511    Returns
512    -------
513    out : array
514        Image array after rescaling its intensity. This image is the same dtype
515        as the input image.
516
517    Notes
518    -----
519    .. versionchanged:: 0.17
520        The dtype of the output array has changed to match the input dtype, or
521        float if the output range is specified by a pair of floats.
522
523    See Also
524    --------
525    equalize_hist
526
527    Examples
528    --------
529    By default, the min/max intensities of the input image are stretched to
530    the limits allowed by the image's dtype, since `in_range` defaults to
531    'image' and `out_range` defaults to 'dtype':
532
533    >>> image = np.array([51, 102, 153], dtype=np.uint8)
534    >>> rescale_intensity(image)
535    array([  0, 127, 255], dtype=uint8)
536
537    It's easy to accidentally convert an image dtype from uint8 to float:
538
539    >>> 1.0 * image
540    array([ 51., 102., 153.])
541
542    Use `rescale_intensity` to rescale to the proper range for float dtypes:
543
544    >>> image_float = 1.0 * image
545    >>> rescale_intensity(image_float)
546    array([0. , 0.5, 1. ])
547
548    To maintain the low contrast of the original, use the `in_range` parameter:
549
550    >>> rescale_intensity(image_float, in_range=(0, 255))
551    array([0.2, 0.4, 0.6])
552
553    If the min/max value of `in_range` is more/less than the min/max image
554    intensity, then the intensity levels are clipped:
555
556    >>> rescale_intensity(image_float, in_range=(0, 102))
557    array([0.5, 1. , 1. ])
558
559    If you have an image with signed integers but want to rescale the image to
560    just the positive range, use the `out_range` parameter. In that case, the
561    output dtype will be float:
562
563    >>> image = np.array([-10, 0, 10], dtype=np.int8)
564    >>> rescale_intensity(image, out_range=(0, 127))
565    array([  0. ,  63.5, 127. ])
566
567    To get the desired range with a specific dtype, use ``.astype()``:
568
569    >>> rescale_intensity(image, out_range=(0, 127)).astype(np.int8)
570    array([  0,  63, 127], dtype=int8)
571
572    If the input image is constant, the output will be clipped directly to the
573    output range:
574    >>> image = np.array([130, 130, 130], dtype=np.int32)
575    >>> rescale_intensity(image, out_range=(0, 127)).astype(np.int32)
576    array([127, 127, 127], dtype=int32)
577    """
578    if out_range in ['dtype', 'image']:
579        out_dtype = _output_dtype(image.dtype.type, image.dtype)
580    else:
581        out_dtype = _output_dtype(out_range, image.dtype)
582
583    imin, imax = map(float, intensity_range(image, in_range))
584    omin, omax = map(float, intensity_range(image, out_range,
585                                            clip_negative=(imin >= 0)))
586
587    if np.any(np.isnan([imin, imax, omin, omax])):
588        utils.warn(
589            "One or more intensity levels are NaN. Rescaling will broadcast "
590            "NaN to the full image. Provide intensity levels yourself to "
591            "avoid this. E.g. with np.nanmin(image), np.nanmax(image).",
592            stacklevel=2
593        )
594
595    image = np.clip(image, imin, imax)
596
597    if imin != imax:
598        image = (image - imin) / (imax - imin)
599        return np.asarray(image * (omax - omin) + omin, dtype=out_dtype)
600    else:
601        return np.clip(image, omin, omax).astype(out_dtype)
602
603
604def _assert_non_negative(image):
605
606    if np.any(image < 0):
607        raise ValueError('Image Correction methods work correctly only on '
608                         'images with non-negative values. Use '
609                         'skimage.exposure.rescale_intensity.')
610
611
612def _adjust_gamma_u8(image, gamma, gain):
613    """LUT based implementation of gamma adjustment.
614
615    """
616    lut = (255 * gain * (np.linspace(0, 1, 256) ** gamma))
617    lut = np.minimum(lut, 255).astype('uint8')
618    return lut[image]
619
620
621def adjust_gamma(image, gamma=1, gain=1):
622    """Performs Gamma Correction on the input image.
623
624    Also known as Power Law Transform.
625    This function transforms the input image pixelwise according to the
626    equation ``O = I**gamma`` after scaling each pixel to the range 0 to 1.
627
628    Parameters
629    ----------
630    image : ndarray
631        Input image.
632    gamma : float, optional
633        Non negative real number. Default value is 1.
634    gain : float, optional
635        The constant multiplier. Default value is 1.
636
637    Returns
638    -------
639    out : ndarray
640        Gamma corrected output image.
641
642    See Also
643    --------
644    adjust_log
645
646    Notes
647    -----
648    For gamma greater than 1, the histogram will shift towards left and
649    the output image will be darker than the input image.
650
651    For gamma less than 1, the histogram will shift towards right and
652    the output image will be brighter than the input image.
653
654    References
655    ----------
656    .. [1] https://en.wikipedia.org/wiki/Gamma_correction
657
658    Examples
659    --------
660    >>> from skimage import data, exposure, img_as_float
661    >>> image = img_as_float(data.moon())
662    >>> gamma_corrected = exposure.adjust_gamma(image, 2)
663    >>> # Output is darker for gamma > 1
664    >>> image.mean() > gamma_corrected.mean()
665    True
666    """
667    if gamma < 0:
668        raise ValueError("Gamma should be a non-negative real number.")
669
670    dtype = image.dtype.type
671
672    if dtype is np.uint8:
673        out = _adjust_gamma_u8(image, gamma, gain)
674    else:
675        _assert_non_negative(image)
676
677        scale = float(dtype_limits(image, True)[1]
678                      - dtype_limits(image, True)[0])
679
680        out = (((image / scale) ** gamma) * scale * gain).astype(dtype)
681
682    return out
683
684
685def adjust_log(image, gain=1, inv=False):
686    """Performs Logarithmic correction on the input image.
687
688    This function transforms the input image pixelwise according to the
689    equation ``O = gain*log(1 + I)`` after scaling each pixel to the range
690    0 to 1. For inverse logarithmic correction, the equation is
691    ``O = gain*(2**I - 1)``.
692
693    Parameters
694    ----------
695    image : ndarray
696        Input image.
697    gain : float, optional
698        The constant multiplier. Default value is 1.
699    inv : float, optional
700        If True, it performs inverse logarithmic correction,
701        else correction will be logarithmic. Defaults to False.
702
703    Returns
704    -------
705    out : ndarray
706        Logarithm corrected output image.
707
708    See Also
709    --------
710    adjust_gamma
711
712    References
713    ----------
714    .. [1] http://www.ece.ucsb.edu/Faculty/Manjunath/courses/ece178W03/EnhancePart1.pdf
715
716    """
717    _assert_non_negative(image)
718    dtype = image.dtype.type
719    scale = float(dtype_limits(image, True)[1] - dtype_limits(image, True)[0])
720
721    if inv:
722        out = (2 ** (image / scale) - 1) * scale * gain
723        return dtype(out)
724
725    out = np.log2(1 + image / scale) * scale * gain
726    return out.astype(dtype)
727
728
729def adjust_sigmoid(image, cutoff=0.5, gain=10, inv=False):
730    """Performs Sigmoid Correction on the input image.
731
732    Also known as Contrast Adjustment.
733    This function transforms the input image pixelwise according to the
734    equation ``O = 1/(1 + exp*(gain*(cutoff - I)))`` after scaling each pixel
735    to the range 0 to 1.
736
737    Parameters
738    ----------
739    image : ndarray
740        Input image.
741    cutoff : float, optional
742        Cutoff of the sigmoid function that shifts the characteristic curve
743        in horizontal direction. Default value is 0.5.
744    gain : float, optional
745        The constant multiplier in exponential's power of sigmoid function.
746        Default value is 10.
747    inv : bool, optional
748        If True, returns the negative sigmoid correction. Defaults to False.
749
750    Returns
751    -------
752    out : ndarray
753        Sigmoid corrected output image.
754
755    See Also
756    --------
757    adjust_gamma
758
759    References
760    ----------
761    .. [1] Gustav J. Braun, "Image Lightness Rescaling Using Sigmoidal Contrast
762           Enhancement Functions",
763           http://markfairchild.org/PDFs/PAP07.pdf
764
765    """
766    _assert_non_negative(image)
767    dtype = image.dtype.type
768    scale = float(dtype_limits(image, True)[1] - dtype_limits(image, True)[0])
769
770    if inv:
771        out = (1 - 1 / (1 + np.exp(gain * (cutoff - image / scale)))) * scale
772        return dtype(out)
773
774    out = (1 / (1 + np.exp(gain * (cutoff - image / scale)))) * scale
775    return out.astype(dtype)
776
777
778def is_low_contrast(image, fraction_threshold=0.05, lower_percentile=1,
779                    upper_percentile=99, method='linear'):
780    """Determine if an image is low contrast.
781
782    Parameters
783    ----------
784    image : array-like
785        The image under test.
786    fraction_threshold : float, optional
787        The low contrast fraction threshold. An image is considered low-
788        contrast when its range of brightness spans less than this
789        fraction of its data type's full range. [1]_
790    lower_percentile : float, optional
791        Disregard values below this percentile when computing image contrast.
792    upper_percentile : float, optional
793        Disregard values above this percentile when computing image contrast.
794    method : str, optional
795        The contrast determination method.  Right now the only available
796        option is "linear".
797
798    Returns
799    -------
800    out : bool
801        True when the image is determined to be low contrast.
802
803    Notes
804    -----
805    For boolean images, this function returns False only if all values are
806    the same (the method, threshold, and percentile arguments are ignored).
807
808    References
809    ----------
810    .. [1] https://scikit-image.org/docs/dev/user_guide/data_types.html
811
812    Examples
813    --------
814    >>> image = np.linspace(0, 0.04, 100)
815    >>> is_low_contrast(image)
816    True
817    >>> image[-1] = 1
818    >>> is_low_contrast(image)
819    True
820    >>> is_low_contrast(image, upper_percentile=100)
821    False
822    """
823    image = np.asanyarray(image)
824
825    if image.dtype == bool:
826        return not ((image.max() == 1) and (image.min() == 0))
827
828    if image.ndim == 3:
829        if image.shape[2] == 4:
830            image = rgba2rgb(image)
831        if image.shape[2] == 3:
832            image = rgb2gray(image)
833
834    dlimits = dtype_limits(image, clip_negative=False)
835    limits = np.percentile(image, [lower_percentile, upper_percentile])
836    ratio = (limits[1] - limits[0]) / (dlimits[1] - dlimits[0])
837
838    return ratio < fraction_threshold
839