Aluode/PerceptionLabPortable
0
1import numpy2 3from .Qt import QtGui4from . import functions5from .util.cupy_helper import getCupy6from .util.numba_helper import getNumbaFunctions7 8 9def _apply_lut_for_uint(xp, image, lut):10 # Note: compared to makeARGB(), we have already clipped the data to range11 12 # if lut is 1d, then lut[image] is fastest13 # if lut is 2d, then lut.take(image, axis=0) is faster than lut[image]14 lut = _convert_2dlut_to_1dlut(xp, lut)15 16 if xp == numpy and (fn_numba := getNumbaFunctions()) is not None:17 # numba "take" supports only the 1st 2 arguments of np.take,18 # therefore we have to convert the lut to 1d.19 # "take" will output a c contiguous array regardless of its input.20 image = fn_numba.numba_take(lut, image)21 else:22 # advanced indexing is memory order aware.23 # its output can be either C or F contiguous.24 image = lut[image]25 26 if image.dtype == xp.uint32:27 # "view" requires c contiguous for numpy < 1.2328 image = xp.ascontiguousarray(image)29 image = image[..., xp.newaxis].view(xp.uint8)30 31 return image32 33 34def _convert_lut_to_rgba(xp, lut):35 # converts:36 # - None to (256, 4)37 # - uint8 (N,) to uint8 (N, 4)38 # - uint8 (N, 1) to uint8 (N, 4)39 # - uint8 (N, 3) to uint8 (N, 4)40 41 if not (42 lut is None43 or lut.ndim == 144 or (45 lut.ndim == 246 and lut.shape[1] in (1, 3, 4)47 )48 ):49 raise ValueError("unsupported lut shape")50 51 N = lut.shape[0] if lut is not None else 25652 53 if lut is None:54 lut = xp.arange(N, dtype=xp.uint8)55 56 # convert (N,) to (N, 1)57 if lut.ndim == 1:58 lut = lut[:, xp.newaxis]59 60 if lut.shape[1] == 4:61 return lut62 63 out = xp.full((N, 4), 255, dtype=xp.uint8)64 out[:, 0:3] = lut65 return out66 67 68def _convert_2dlut_to_1dlut(xp, lut):69 # converts:70 # - uint8 (N, 1) to uint8 (N,)71 # - uint8 (N, 3) or (N, 4) to uint32 (N,)72 # this allows faster lookup as 1d lookup is faster73 74 if lut.ndim == 1:75 return lut76 77 if lut.shape[1] == 3: # rgb78 # convert rgb lut to rgba so that it is 32-bits79 lut = xp.column_stack([lut, xp.full(lut.shape[0], 255, dtype=xp.uint8)])80 if lut.shape[1] == 4: # rgba81 lut = lut.view(xp.uint32)82 lut = lut.ravel()83 84 return lut85 86 87def _rescale_and_lookup_float(xp, image, levels, lut, *, forceApplyLut):88 # It is usually more performant to _not_ apply the lut and89 # instead use it as an Indexed8 ColorTable. This is only90 # applicable if the lut has <= 256 entries.91 92 if forceApplyLut and lut is None:93 raise ValueError("forceApplyLut True but lut not provided")94 95 # Decide on maximum scaled value96 if lut is not None:97 num_colors = lut.shape[0]98 max_scale_value = num_colors99 else:100 num_colors = 256101 max_scale_value = 255.0102 dtype = xp.min_scalar_type(num_colors - 1)103 104 # note: "dtype == uint16" ==> lut provided ==> mono-channel image105 # i.e. multi-channel image ==> lut is None ==> dtype == uint8106 #107 # the library defaults to using 256-entry luts, so108 # "dtype == uint8" is the common case109 110 apply_lut = forceApplyLut or dtype == xp.uint16111 112 minVal, maxVal = levels113 rng = maxVal - minVal114 rng = 1 if rng == 0 else rng115 offset = minVal116 scale = max_scale_value / rng117 118 if xp == numpy and (fn_numba := getNumbaFunctions()) is not None:119 if apply_lut:120 # this path does rescale and apply lut in one step121 lut = _convert_2dlut_to_1dlut(xp, lut)122 image = fn_numba.rescale_and_lookup(image, scale, offset, lut)123 lut = None124 if image.dtype == xp.uint32:125 # "view" requires c contiguous for numpy < 1.23126 image = xp.ascontiguousarray(image)127 image = image[..., xp.newaxis].view(xp.uint8)128 else:129 image = fn_numba.rescale_and_clip(image, scale, offset, 0, num_colors - 1)130 else:131 image = functions.rescaleData(132 image, scale, offset, dtype=dtype, clip=(0, num_colors - 1)133 )134 if apply_lut:135 image = _apply_lut_for_uint(xp, image, lut)136 lut = None137 138 # image is now of type uint8139 return image, lut140 141 142def _combine_levels_and_lut(xp, image, levels, lut):143 if (144 image.dtype == xp.uint16145 and levels is None146 and image.ndim == 3147 and image.shape[2] == 3148 ):149 # uint16 rgb can't be directly displayed, so make it150 # pass through effective lut processing151 levels = [0, 65535]152 153 if levels is None and lut is None:154 # nothing to combine155 return image, lut156 157 # distinguish between lut for levels and colors158 levels_lut = None159 colors_lut = lut160 161 eflsize = 2 ** (image.itemsize * 8)162 if levels is None:163 info = xp.iinfo(image.dtype)164 minlev, maxlev = info.min, info.max165 else:166 minlev, maxlev = levels167 levdiff = maxlev - minlev168 levdiff = 1 if levdiff == 0 else levdiff # don't allow division by 0169 offset = minlev170 171 if colors_lut is None:172 scale = 255.0 / levdiff173 if image.dtype == xp.ubyte and image.ndim == 2:174 # uint8 mono image175 ind = xp.arange(eflsize)176 levels_lut = functions.rescaleData(ind, scale, offset, dtype=xp.ubyte)177 # image data is not scaled. instead, levels_lut is used178 # as (grayscale) Indexed8 ColorTable to get the same effect.179 # due to the small size of the input to rescaleData(), we180 # do not bother caching the result181 return image, levels_lut182 else:183 # uint16 mono, uint8 rgb, uint16 rgb184 # rescale image data by computation instead of by memory lookup185 if xp == numpy and (fn_numba := getNumbaFunctions()) is not None:186 image = fn_numba.rescale_and_clip(image, scale, offset, 0, 255)187 else:188 image = functions.rescaleData(image, scale, offset, dtype=xp.ubyte)189 return image, colors_lut190 else:191 num_colors = colors_lut.shape[0]192 scale = num_colors / levdiff193 lutdtype = xp.min_scalar_type(num_colors - 1)194 195 if image.dtype == xp.ubyte or lutdtype != xp.ubyte:196 # combine if either:197 # 1) uint8 mono image198 # 2) colors_lut has more entries than will fit within 8-bits199 ind = xp.arange(eflsize)200 levels_lut = functions.rescaleData(201 ind, scale, offset, dtype=lutdtype, clip=(0, num_colors - 1),202 )203 efflut = colors_lut[levels_lut]204 205 # apply the effective lut early for the following types:206 if image.dtype == xp.uint16 and image.ndim == 2:207 image = _apply_lut_for_uint(xp, image, efflut)208 efflut = None209 return image, efflut210 else:211 # uint16 image with colors_lut <= 256 entries212 # don't combine, we will use QImage ColorTable213 if xp == numpy and (fn_numba := getNumbaFunctions()) is not None:214 image = fn_numba.rescale_and_clip(image, scale, offset, 0, num_colors - 1)215 else:216 image = functions.rescaleData(217 image, scale, offset, dtype=lutdtype, clip=(0, num_colors - 1),218 )219 return image, colors_lut220 221 222def try_make_qimage(image, *, levels, lut, transparentLocations=None):223 """224 Internal function to make an QImage from an ndarray without going225 through the full generality of makeARGB().226 Only certain combinations of input arguments are supported.227 """228 229 # this function assumes that image has no nans.230 # checking for nans is an expensive operation; it is expected that231 # the caller would want to cache the result rather than have this232 # function check for nans unconditionally.233 234 cp = getCupy()235 xp = cp.get_array_module(image) if cp else numpy236 237 # float images always need levels238 if image.dtype.kind == "f" and levels is None:239 return None240 241 if levels is not None:242 levels = xp.asarray(levels)243 244 # can't handle multi-channel levels245 if levels.ndim != 1:246 return None247 248 # if levels is provided, multi-channel images must be 3 channels only.249 # (because it doesn't make sense to scale a 4th alpha channel.)250 if image.ndim == 3 and image.shape[2] != 3:251 return None252 253 if lut is not None and lut.dtype != xp.uint8:254 raise ValueError("lut dtype must be uint8")255 256 alpha_channel_required = (257 ( # image itself has alpha channel258 image.ndim == 3259 and image.shape[2] == 4260 )261 or262 ( # lut has alpha channel263 lut is not None264 and lut.ndim == 2265 and lut.shape[1] == 4266 )267 )268 269 if image.dtype.kind == "f":270 if image.ndim == 2:271 # mono float images272 if transparentLocations is None:273 image, lut = _rescale_and_lookup_float(274 xp, image, levels, lut, forceApplyLut=False275 )276 levels = None277 # on return, we will have an uint8 image.278 # lut if not None will have <= 256 entries279 else:280 # this path creates an alpha channel281 lut = _convert_lut_to_rgba(xp, lut)282 alpha_channel_required = True283 284 image, lut = _rescale_and_lookup_float(285 xp, image, levels, lut, forceApplyLut=True286 )287 levels = None288 assert lut is None289 image[..., 3][transparentLocations] = 0290 else:291 # RGB float images292 # lut can only be None for RGB images293 image, lut = _rescale_and_lookup_float(294 xp, image, levels, lut, forceApplyLut=False295 )296 levels = None297 298 if transparentLocations is not None:299 alpha_channel_required = True300 mask = xp.full(image.shape[:2], 255, dtype=xp.uint8)301 mask[transparentLocations] = 0302 image = xp.dstack((image, mask))303 304 # if the image data is a small int, then we can combine levels + lut305 # into a single lut for better performance306 elif image.dtype in (xp.ubyte, xp.uint16):307 image, lut = _combine_levels_and_lut(xp, image, levels, lut)308 levels = None309 310 ubyte_nolvl = image.dtype == xp.ubyte and levels is None311 is_passthru8 = ubyte_nolvl and lut is None312 is_indexed8 = (313 ubyte_nolvl and image.ndim == 2 and lut is not None and lut.shape[0] <= 256314 )315 is_passthru16 = image.dtype == xp.uint16 and levels is None and lut is None316 can_grayscale16 = (317 is_passthru16318 and image.ndim == 2319 and hasattr(QtGui.QImage.Format, "Format_Grayscale16")320 )321 is_rgba64 = is_passthru16 and image.ndim == 3 and image.shape[2] == 4322 323 # bypass makeARGB for supported combinations324 supported = is_passthru8 or is_indexed8 or can_grayscale16 or is_rgba64325 if not supported:326 return None327 328 if xp == cp:329 image = image.get()330 331 # worthwhile supporting non-contiguous arrays332 image = numpy.ascontiguousarray(image)333 334 fmt = None335 ctbl = None336 if is_passthru8:337 # both levels and lut are None338 # these images are suitable for display directly339 if image.ndim == 2:340 fmt = QtGui.QImage.Format.Format_Grayscale8341 elif image.shape[2] == 3:342 fmt = QtGui.QImage.Format.Format_RGB888343 elif image.shape[2] == 4:344 if alpha_channel_required:345 fmt = QtGui.QImage.Format.Format_RGBA8888346 else:347 fmt = QtGui.QImage.Format.Format_RGBX8888348 elif is_indexed8:349 # levels and/or lut --> lut-only350 fmt = QtGui.QImage.Format.Format_Indexed8351 if lut.ndim == 1 or lut.shape[1] == 1:352 ctbl = [QtGui.qRgb(x, x, x) for x in lut.ravel().tolist()]353 elif lut.shape[1] == 3:354 ctbl = [QtGui.qRgb(*rgb) for rgb in lut.tolist()]355 elif lut.shape[1] == 4:356 ctbl = [QtGui.qRgba(*rgba) for rgba in lut.tolist()]357 elif can_grayscale16:358 # single channel uint16359 # both levels and lut are None360 fmt = QtGui.QImage.Format.Format_Grayscale16361 elif is_rgba64:362 # uint16 rgba363 # both levels and lut are None364 fmt = QtGui.QImage.Format.Format_RGBA64 # endian-independent365 if fmt is None:366 raise ValueError("unsupported image type")367 qimage = functions.ndarray_to_qimage(image, fmt)368 if ctbl is not None:369 qimage.setColorTable(ctbl)370 return qimage371 