Skip to content

Commit 81ce9f6

Browse files
committed
vendored code from scikit-image for rescale_intensity
1 parent 01195f8 commit 81ce9f6

File tree

2 files changed

+249
-16
lines changed

2 files changed

+249
-16
lines changed

packages/python/plotly/plotly/express/_imshow.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from PIL import Image
66
from io import BytesIO
77
import base64
8-
from skimage.exposure import rescale_intensity
8+
from .imshow_utils import rescale_intensity, _integer_ranges, _integer_types
99
import pandas as pd
1010

1111
try:
@@ -17,21 +17,6 @@
1717

1818
_float_types = []
1919

20-
# Adapted from skimage.util.dtype
21-
_integer_types = (
22-
np.byte,
23-
np.ubyte, # 8 bits
24-
np.short,
25-
np.ushort, # 16 bits
26-
np.intc,
27-
np.uintc, # 16 or 32 or 64 bits
28-
np.int_,
29-
np.uint, # 32 or 64 bits
30-
np.longlong,
31-
np.ulonglong,
32-
) # 64 bits
33-
_integer_ranges = {t: (np.iinfo(t).min, np.iinfo(t).max) for t in _integer_types}
34-
3520

3621
def _array_to_b64str(img, ext="png"):
3722
pil_img = Image.fromarray(img)
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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

Comments
 (0)