CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
functions_qimage.py371 linesDownload Raw Back to pyqtgraph
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 
Aluode/PerceptionLabPortable · CoolFace