|
6 | 6 |
|
7 | 7 | import numpy as np |
8 | 8 |
|
9 | | -from ._warnings import all_warnings, warn |
| 9 | +from ._warnings import all_warnings, warn, warn_external |
| 10 | + |
10 | 11 |
|
11 | 12 | __all__ = [ |
12 | 13 | 'deprecate_func', |
@@ -968,6 +969,7 @@ def convert_to_float(image, preserve_range): |
968 | 969 | if image.dtype.char not in 'df': |
969 | 970 | image = image.astype(float) |
970 | 971 | else: |
| 972 | + # Avoid circular import |
971 | 973 | from ..util.dtype import img_as_float |
972 | 974 |
|
973 | 975 | image = img_as_float(image) |
@@ -1113,3 +1115,174 @@ def as_binary_ndarray(array, *, variable_name): |
1113 | 1115 | f"safely cast to boolean array." |
1114 | 1116 | ) |
1115 | 1117 | 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