Skip to content

Commit 5d0aded

Browse files
lagrumatthew-brettmkcorstefanv
authored
Add prescale parameter to "blob functions" (scikit-image#7858)
Implementation of the new strategy around value range scaling in our library that we extensively discussed in the recent sprint and meeting. We will add more detailed descriptions of this strategy but in short: - introduce a new `prescale` parameter for functions that actually need / benefit from prescaling the input image. - Add a dedicated private (for now) function for that. I plan to use this elsewhere in the future. Co-authored-by: Matthew Brett <matthew.brett@gmail.com> Co-authored-by: Marianne Corvellec <marianne.corvellec@ens-lyon.org> Co-authored-by: Stefan van der Walt <stefan@mentat.za.net>
1 parent b118e59 commit 5d0aded

File tree

5 files changed

+528
-133
lines changed

5 files changed

+528
-133
lines changed

TODO.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ Version 0.27
2424
- Remove `doc/source/user_guide/plugins.rst`
2525
* Complete deprecation of `square`, `rectangle` and `cube` in `skimage.morphology`.
2626

27+
Version 0.29
28+
------------
29+
* Complete deprecation of `threshold_rel` in `skimage.feature.blob_*` functions
30+
2731
Other
2832
-----
2933
* Once NumPy 1.25.0 is minimal required version:
@@ -60,6 +64,7 @@ Post numpy 2
6064
- Remove references to `numpy.bool8` once NumPy 2.0 is minimal required version.
6165
- scipy>=1.12: remove SCIPY_CG_TOL_PARAM_NAME in `_shared.compat.py`.
6266
- Remove `np2` check in `skimage/feature/brief.py`.
67+
- Consider using `numpy.testing.assert_equal()` with `strict=True` everywhere.
6368

6469
Version 2.0
6570
-----------

src/skimage/_shared/utils.py

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
import numpy as np
88

9-
from ._warnings import all_warnings, warn
9+
from ._warnings import all_warnings, warn, warn_external
10+
1011

1112
__all__ = [
1213
'deprecate_func',
@@ -968,6 +969,7 @@ def convert_to_float(image, preserve_range):
968969
if image.dtype.char not in 'df':
969970
image = image.astype(float)
970971
else:
972+
# Avoid circular import
971973
from ..util.dtype import img_as_float
972974

973975
image = img_as_float(image)
@@ -1113,3 +1115,174 @@ def as_binary_ndarray(array, *, variable_name):
11131115
f"safely cast to boolean array."
11141116
)
11151117
return np.asarray(array, dtype=bool)
1118+
1119+
1120+
def _minmax_scale_value_range(image):
1121+
"""Rescale `image` to the value range [0, 1].
1122+
1123+
Rescaling values between [0, 1], or *min-max normalization* [1]_,
1124+
is a simple method to ensure that data is inside a range.
1125+
1126+
Parameters
1127+
----------
1128+
image : ndarray
1129+
Input image.
1130+
1131+
Returns
1132+
-------
1133+
rescaled_image : ndarray
1134+
Rescaled image, of same shape as input `image` but with a
1135+
floating dtype (according to :func:`_supported_float_type`).
1136+
1137+
Raises
1138+
------
1139+
ValueError
1140+
Rescaling an image that contains NaN or infinity is not supported for
1141+
now. In those cases, consider replacing the unsupported values manually.
1142+
1143+
See Also
1144+
--------
1145+
_rescale_value_range
1146+
Rescale the value range of `image` according to the selected `mode`.
1147+
1148+
References
1149+
----------
1150+
.. [1]: https://en.wikipedia.org/wiki/Feature_scaling#Rescaling_(min-max_normalization)
1151+
1152+
Examples
1153+
--------
1154+
>>> import numpy as np
1155+
>>> image = np.array([-10, 45, 100], dtype=np.int8)
1156+
>>> _minmax_scale_value_range(image)
1157+
array([0. , 0.5, 1. ])
1158+
"""
1159+
# Prepare `out` array, `lower` and `higher` with exact dtype to avoid
1160+
# unexpected promotion and / or precision problems during normalization
1161+
dtype = _supported_float_type(image.dtype, allow_complex=False)
1162+
out = image.astype(dtype)
1163+
1164+
lower = out.min()
1165+
higher = out.max()
1166+
1167+
# Deal with unexpected or invalid `lower` and `higher` early
1168+
if np.isnan(lower) or np.isnan(higher):
1169+
msg = (
1170+
"`image` contains NaN. "
1171+
"Min-max normalization with NaN is not supported. "
1172+
"Replace NaNs manually before rescaling."
1173+
)
1174+
raise ValueError(msg)
1175+
1176+
if np.isinf(lower) or np.isinf(higher):
1177+
msg = (
1178+
"`image` contains inf. "
1179+
"Min-max normalization with inf is not supported. "
1180+
"Replace inf manually before rescaling."
1181+
)
1182+
raise ValueError(msg)
1183+
1184+
if lower == higher:
1185+
msg = "`image` is uniform, returning uniform array of 0's"
1186+
warn_external(msg, category=RuntimeWarning)
1187+
out = np.zeros_like(out)
1188+
return out
1189+
assert lower < higher
1190+
1191+
# Actual normalization
1192+
with np.errstate(all="raise"):
1193+
try:
1194+
peak_to_peak = higher - lower
1195+
out -= lower
1196+
except FloatingPointError as e:
1197+
if "overflow" in e.args[0]:
1198+
warn_external(
1199+
"Overflow while attempting to rescale. This could be due to "
1200+
"`image` containing unexpectedly large values. Dividing all "
1201+
"values by 2 before rescaling to avoid overflow.",
1202+
category=RuntimeWarning,
1203+
)
1204+
out /= 2
1205+
lower /= 2
1206+
higher /= 2
1207+
peak_to_peak = higher - lower
1208+
out -= lower
1209+
else:
1210+
raise
1211+
1212+
out /= peak_to_peak
1213+
1214+
return out
1215+
1216+
1217+
def _rescale_value_range(image, *, mode):
1218+
"""Rescale the value range of `image` according to the selected `mode`.
1219+
1220+
For now, this private function handles *prescaling* for public API that
1221+
needs a value range to be known and well-defined.
1222+
1223+
Parameters
1224+
----------
1225+
image : ndarray
1226+
Image to rescale.
1227+
mode : {'minmax', 'none', 'legacy'}, optional
1228+
Controls the rescaling behavior for `image`.
1229+
1230+
``'minmax'``
1231+
Normalize `image` between 0 and 1 regardless of dtype. After
1232+
normalization, `rescaled_image` will have a floating dtype
1233+
(according to :func:`_supported_float_type`).
1234+
1235+
``'none'``
1236+
Don't rescale the value range of `image` at all and return a
1237+
copy of `image`. Useful when `image` has already been rescaled.
1238+
1239+
``'legacy'``
1240+
Normalize only if `image` has an integer dtype. If `image` is of
1241+
floating dtype, it is left alone. See :func:`.img_as_float` for
1242+
more details.
1243+
1244+
Returns
1245+
-------
1246+
rescaled_image : ndarray
1247+
The rescaled `image` of the same shape but possibly with a different
1248+
dtype.
1249+
1250+
Raises
1251+
------
1252+
ValueError
1253+
Rescaling an `image` with `mode='minmax'` that contains NaN or
1254+
infinity is not supported for now. In those cases, consider replacing
1255+
the unsupported values manually.
1256+
1257+
See Also
1258+
--------
1259+
_minmax_scale_value_range
1260+
Rescale `image` to the value range [0, 1]. Internally used in
1261+
this function.
1262+
1263+
Examples
1264+
--------
1265+
>>> import numpy as np
1266+
>>> image = np.array([-10, 45, 100], dtype=np.int8)
1267+
1268+
>>> _rescale_value_range(image, mode="minmax")
1269+
array([0. , 0.5, 1. ])
1270+
1271+
>>> _rescale_value_range(image, mode="legacy")
1272+
array([-0.07874016, 0.35433071, 0.78740157])
1273+
1274+
>>> _rescale_value_range(image, mode="none")
1275+
array([-10, 45, 100], dtype=int8)
1276+
"""
1277+
if mode == "none":
1278+
# Exit early
1279+
return image.copy()
1280+
if mode == "legacy":
1281+
# Avoid circular import
1282+
from ..util.dtype import img_as_float
1283+
1284+
return img_as_float(image)
1285+
if mode == "minmax":
1286+
return _minmax_scale_value_range(image)
1287+
else:
1288+
raise ValueError("unsupported mode")

0 commit comments

Comments
 (0)