|
| 1 | +"""Vendored code from scikit-image in order to limit the number of dependencies |
| 2 | +Extracted from scikit-image/skimage/exposure/exposure.py |
| 3 | +""" |
| 4 | + |
| 5 | +import numpy as np |
| 6 | + |
| 7 | +from warnings import warn |
| 8 | + |
| 9 | +_integer_types = ( |
| 10 | + np.byte, |
| 11 | + np.ubyte, # 8 bits |
| 12 | + np.short, |
| 13 | + np.ushort, # 16 bits |
| 14 | + np.intc, |
| 15 | + np.uintc, # 16 or 32 or 64 bits |
| 16 | + np.int_, |
| 17 | + np.uint, # 32 or 64 bits |
| 18 | + np.longlong, |
| 19 | + np.ulonglong, |
| 20 | +) # 64 bits |
| 21 | +_integer_ranges = {t: (np.iinfo(t).min, np.iinfo(t).max) for t in _integer_types} |
| 22 | +dtype_range = { |
| 23 | + np.bool_: (False, True), |
| 24 | + np.bool8: (False, True), |
| 25 | + np.float16: (-1, 1), |
| 26 | + np.float32: (-1, 1), |
| 27 | + np.float64: (-1, 1), |
| 28 | +} |
| 29 | +dtype_range.update(_integer_ranges) |
| 30 | + |
| 31 | + |
| 32 | +DTYPE_RANGE = dtype_range.copy() |
| 33 | +DTYPE_RANGE.update((d.__name__, limits) for d, limits in dtype_range.items()) |
| 34 | +DTYPE_RANGE.update( |
| 35 | + { |
| 36 | + "uint10": (0, 2 ** 10 - 1), |
| 37 | + "uint12": (0, 2 ** 12 - 1), |
| 38 | + "uint14": (0, 2 ** 14 - 1), |
| 39 | + "bool": dtype_range[np.bool_], |
| 40 | + "float": dtype_range[np.float64], |
| 41 | + } |
| 42 | +) |
| 43 | + |
| 44 | + |
| 45 | +def intensity_range(image, range_values="image", clip_negative=False): |
| 46 | + """Return image intensity range (min, max) based on desired value type. |
| 47 | +
|
| 48 | + Parameters |
| 49 | + ---------- |
| 50 | + image : array |
| 51 | + Input image. |
| 52 | + range_values : str or 2-tuple, optional |
| 53 | + The image intensity range is configured by this parameter. |
| 54 | + The possible values for this parameter are enumerated below. |
| 55 | +
|
| 56 | + 'image' |
| 57 | + Return image min/max as the range. |
| 58 | + 'dtype' |
| 59 | + Return min/max of the image's dtype as the range. |
| 60 | + dtype-name |
| 61 | + Return intensity range based on desired `dtype`. Must be valid key |
| 62 | + in `DTYPE_RANGE`. Note: `image` is ignored for this range type. |
| 63 | + 2-tuple |
| 64 | + Return `range_values` as min/max intensities. Note that there's no |
| 65 | + reason to use this function if you just want to specify the |
| 66 | + intensity range explicitly. This option is included for functions |
| 67 | + that use `intensity_range` to support all desired range types. |
| 68 | +
|
| 69 | + clip_negative : bool, optional |
| 70 | + If True, clip the negative range (i.e. return 0 for min intensity) |
| 71 | + even if the image dtype allows negative values. |
| 72 | + """ |
| 73 | + if range_values == "dtype": |
| 74 | + range_values = image.dtype.type |
| 75 | + |
| 76 | + if range_values == "image": |
| 77 | + i_min = np.min(image) |
| 78 | + i_max = np.max(image) |
| 79 | + elif range_values in DTYPE_RANGE: |
| 80 | + i_min, i_max = DTYPE_RANGE[range_values] |
| 81 | + if clip_negative: |
| 82 | + i_min = 0 |
| 83 | + else: |
| 84 | + i_min, i_max = range_values |
| 85 | + return i_min, i_max |
| 86 | + |
| 87 | + |
| 88 | +def _output_dtype(dtype_or_range): |
| 89 | + """Determine the output dtype for rescale_intensity. |
| 90 | +
|
| 91 | + The dtype is determined according to the following rules: |
| 92 | + - if ``dtype_or_range`` is a dtype, that is the output dtype. |
| 93 | + - if ``dtype_or_range`` is a dtype string, that is the dtype used, unless |
| 94 | + it is not a NumPy data type (e.g. 'uint12' for 12-bit unsigned integers), |
| 95 | + in which case the data type that can contain it will be used |
| 96 | + (e.g. uint16 in this case). |
| 97 | + - if ``dtype_or_range`` is a pair of values, the output data type will be |
| 98 | + float. |
| 99 | +
|
| 100 | + Parameters |
| 101 | + ---------- |
| 102 | + dtype_or_range : type, string, or 2-tuple of int/float |
| 103 | + The desired range for the output, expressed as either a NumPy dtype or |
| 104 | + as a (min, max) pair of numbers. |
| 105 | +
|
| 106 | + Returns |
| 107 | + ------- |
| 108 | + out_dtype : type |
| 109 | + The data type appropriate for the desired output. |
| 110 | + """ |
| 111 | + if type(dtype_or_range) in [list, tuple, np.ndarray]: |
| 112 | + # pair of values: always return float. |
| 113 | + return np.float_ |
| 114 | + if type(dtype_or_range) == type: |
| 115 | + # already a type: return it |
| 116 | + return dtype_or_range |
| 117 | + if dtype_or_range in DTYPE_RANGE: |
| 118 | + # string key in DTYPE_RANGE dictionary |
| 119 | + try: |
| 120 | + # if it's a canonical numpy dtype, convert |
| 121 | + return np.dtype(dtype_or_range).type |
| 122 | + except TypeError: # uint10, uint12, uint14 |
| 123 | + # otherwise, return uint16 |
| 124 | + return np.uint16 |
| 125 | + else: |
| 126 | + raise ValueError( |
| 127 | + "Incorrect value for out_range, should be a valid image data " |
| 128 | + f"type or a pair of values, got {dtype_or_range}." |
| 129 | + ) |
| 130 | + |
| 131 | + |
| 132 | +def rescale_intensity(image, in_range="image", out_range="dtype"): |
| 133 | + """Return image after stretching or shrinking its intensity levels. |
| 134 | +
|
| 135 | + The desired intensity range of the input and output, `in_range` and |
| 136 | + `out_range` respectively, are used to stretch or shrink the intensity range |
| 137 | + of the input image. See examples below. |
| 138 | +
|
| 139 | + Parameters |
| 140 | + ---------- |
| 141 | + image : array |
| 142 | + Image array. |
| 143 | + in_range, out_range : str or 2-tuple, optional |
| 144 | + Min and max intensity values of input and output image. |
| 145 | + The possible values for this parameter are enumerated below. |
| 146 | +
|
| 147 | + 'image' |
| 148 | + Use image min/max as the intensity range. |
| 149 | + 'dtype' |
| 150 | + Use min/max of the image's dtype as the intensity range. |
| 151 | + dtype-name |
| 152 | + Use intensity range based on desired `dtype`. Must be valid key |
| 153 | + in `DTYPE_RANGE`. |
| 154 | + 2-tuple |
| 155 | + Use `range_values` as explicit min/max intensities. |
| 156 | +
|
| 157 | + Returns |
| 158 | + ------- |
| 159 | + out : array |
| 160 | + Image array after rescaling its intensity. This image is the same dtype |
| 161 | + as the input image. |
| 162 | +
|
| 163 | + Notes |
| 164 | + ----- |
| 165 | + .. versionchanged:: 0.17 |
| 166 | + The dtype of the output array has changed to match the output dtype, or |
| 167 | + float if the output range is specified by a pair of floats. |
| 168 | +
|
| 169 | + See Also |
| 170 | + -------- |
| 171 | + equalize_hist |
| 172 | +
|
| 173 | + Examples |
| 174 | + -------- |
| 175 | + By default, the min/max intensities of the input image are stretched to |
| 176 | + the limits allowed by the image's dtype, since `in_range` defaults to |
| 177 | + 'image' and `out_range` defaults to 'dtype': |
| 178 | +
|
| 179 | + >>> image = np.array([51, 102, 153], dtype=np.uint8) |
| 180 | + >>> rescale_intensity(image) |
| 181 | + array([ 0, 127, 255], dtype=uint8) |
| 182 | +
|
| 183 | + It's easy to accidentally convert an image dtype from uint8 to float: |
| 184 | +
|
| 185 | + >>> 1.0 * image |
| 186 | + array([ 51., 102., 153.]) |
| 187 | +
|
| 188 | + Use `rescale_intensity` to rescale to the proper range for float dtypes: |
| 189 | +
|
| 190 | + >>> image_float = 1.0 * image |
| 191 | + >>> rescale_intensity(image_float) |
| 192 | + array([0. , 0.5, 1. ]) |
| 193 | +
|
| 194 | + To maintain the low contrast of the original, use the `in_range` parameter: |
| 195 | +
|
| 196 | + >>> rescale_intensity(image_float, in_range=(0, 255)) |
| 197 | + array([0.2, 0.4, 0.6]) |
| 198 | +
|
| 199 | + If the min/max value of `in_range` is more/less than the min/max image |
| 200 | + intensity, then the intensity levels are clipped: |
| 201 | +
|
| 202 | + >>> rescale_intensity(image_float, in_range=(0, 102)) |
| 203 | + array([0.5, 1. , 1. ]) |
| 204 | +
|
| 205 | + If you have an image with signed integers but want to rescale the image to |
| 206 | + just the positive range, use the `out_range` parameter. In that case, the |
| 207 | + output dtype will be float: |
| 208 | +
|
| 209 | + >>> image = np.array([-10, 0, 10], dtype=np.int8) |
| 210 | + >>> rescale_intensity(image, out_range=(0, 127)) |
| 211 | + array([ 0. , 63.5, 127. ]) |
| 212 | +
|
| 213 | + To get the desired range with a specific dtype, use ``.astype()``: |
| 214 | +
|
| 215 | + >>> rescale_intensity(image, out_range=(0, 127)).astype(np.int8) |
| 216 | + array([ 0, 63, 127], dtype=int8) |
| 217 | +
|
| 218 | + If the input image is constant, the output will be clipped directly to the |
| 219 | + output range: |
| 220 | + >>> image = np.array([130, 130, 130], dtype=np.int32) |
| 221 | + >>> rescale_intensity(image, out_range=(0, 127)).astype(np.int32) |
| 222 | + array([127, 127, 127], dtype=int32) |
| 223 | + """ |
| 224 | + if out_range in ["dtype", "image"]: |
| 225 | + out_dtype = _output_dtype(image.dtype.type) |
| 226 | + else: |
| 227 | + out_dtype = _output_dtype(out_range) |
| 228 | + |
| 229 | + imin, imax = map(float, intensity_range(image, in_range)) |
| 230 | + omin, omax = map( |
| 231 | + float, intensity_range(image, out_range, clip_negative=(imin >= 0)) |
| 232 | + ) |
| 233 | + |
| 234 | + if np.any(np.isnan([imin, imax, omin, omax])): |
| 235 | + warn( |
| 236 | + "One or more intensity levels are NaN. Rescaling will broadcast " |
| 237 | + "NaN to the full image. Provide intensity levels yourself to " |
| 238 | + "avoid this. E.g. with np.nanmin(image), np.nanmax(image).", |
| 239 | + stacklevel=2, |
| 240 | + ) |
| 241 | + |
| 242 | + image = np.clip(image, imin, imax) |
| 243 | + |
| 244 | + if imin != imax: |
| 245 | + image = (image - imin) / (imax - imin) |
| 246 | + return np.asarray(image * (omax - omin) + omin, dtype=out_dtype) |
| 247 | + else: |
| 248 | + return np.clip(image, omin, omax).astype(out_dtype) |
0 commit comments