def DataArray2RGB(data, irange=None, tint=(255, 255, 255)): """ :param data: (numpy.ndarray of unsigned int) 2D image greyscale (unsigned float might work as well) :param irange: (None or tuple of 2 values) min/max intensities mapped to black/white None => auto (min, max are from the data); 0, max val of data => whole range is mapped. min must be < max, and must be of the same type as data.dtype. :param tint: (3-tuple of 0 < int <256) RGB colour of the final image (each pixel is multiplied by the value. Default is white. :return: (numpy.ndarray of 3*shape of uint8) converted image in RGB with the same dimension """ # TODO: handle signed values assert (data.ndim == 2) # => 2D with greyscale # Discard the DataArray aspect and just get the raw array, to be sure we # don't get a DataArray as result of the numpy operations data = data.view(numpy.ndarray) # fit it to 8 bits and update brightness and contrast at the same time if irange is None: irange = (numpy.nanmin(data), numpy.nanmax(data)) if math.isnan(irange[0]): logging.warning("Trying to convert all-NaN data to RGB") data = numpy.nan_to_num(data) irange = (0, 1) else: # ensure irange is the same type as the data. It ensures we don't get # crazy values, and also that numpy doesn't get confused in the # intermediary dtype (cf .clip()). irange = numpy.array(irange, data.dtype) # TODO: warn if irange looks too different from original value? if irange[0] == irange[1]: logging.info("Requested RGB conversion with null-range %s", irange) if data.dtype == numpy.uint8 and irange[0] == 0 and irange[1] == 255: # short-cut when data is already the same type # logging.debug("Applying direct range mapping to RGB") drescaled = data # TODO: also write short-cut for 16 bits by reading only the high byte? else: # If data might go outside of the range, clip first if data.dtype.kind in "iu": # no need to clip if irange is the whole possible range idt = numpy.iinfo(data.dtype) # Ensure B&W if there is only one value allowed if irange[0] >= irange[1]: if irange[0] > idt.min: irange = (irange[0] - 1, irange[0]) else: irange = (irange[0], irange[0] + 1) if img_fast: try: # only (currently) supports uint16 return img_fast.DataArray2RGB(data, irange, tint) except ValueError as exp: logging.info("Fast conversion cannot run: %s", exp) except Exception: logging.exception("Failed to use the fast conversion") if irange[0] > idt.min or irange[1] < idt.max: data = data.clip(*irange) else: # floats et al. => always clip # Ensure B&W if there is just one value allowed if irange[0] >= irange[1]: irange = (irange[0] - 1e-9, irange[0]) data = data.clip(*irange) dshift = data - irange[0] if data.dtype == numpy.uint8: drescaled = dshift # re-use memory for the result else: # TODO: could directly use one channel of the 'rgb' variable? drescaled = numpy.empty(data.shape, dtype=numpy.uint8) # Ideally, it would be 255 / (irange[1] - irange[0]) + 0.5, but to avoid # the addition, we can just use 255.99, and with the rounding down, it's # very similar. b = 255.99 / (irange[1] - irange[0]) numpy.multiply(dshift, b, out=drescaled, casting="unsafe") # Now duplicate it 3 times to make it RGB (as a simple approximation of # greyscale) # dstack doesn't work because it doesn't generate in C order (uses strides) # apparently this is as fast (or even a bit better): # 0 copy (1 malloc) rgb = numpy.empty(data.shape + (3, ), dtype=numpy.uint8, order='C') # Tint (colouration) if tint == (255, 255, 255): # fast path when no tint # Note: it seems numpy.repeat() is 10x slower ?! # a = numpy.repeat(drescaled, 3) # a.shape = data.shape + (3,) rgb[:, :, 0] = drescaled # 1 copy rgb[:, :, 1] = drescaled # 1 copy rgb[:, :, 2] = drescaled # 1 copy else: rtint, gtint, btint = tint # multiply by a float, cast back to type of out, and put into out array # TODO: multiplying by float(x/255) is the same as multiplying by int(x) # and >> 8 numpy.multiply(drescaled, rtint / 255, out=rgb[:, :, 0], casting="unsafe") numpy.multiply(drescaled, gtint / 255, out=rgb[:, :, 1], casting="unsafe") numpy.multiply(drescaled, btint / 255, out=rgb[:, :, 2], casting="unsafe") return rgb
def DataArray2RGB(data, irange=None, tint=(255, 255, 255)): """ :param data: (numpy.ndarray of unsigned int) 2D image greyscale (unsigned float might work as well) :param irange: (None or tuple of 2 unsigned int) min/max intensities mapped to black/white None => auto (min, max are from the data); 0, max val of data => whole range is mapped. min must be < max, and must be of the same type as data.dtype. :param tint: (3-tuple of 0 < int <256) RGB colour of the final image (each pixel is multiplied by the value. Default is white. :return: (numpy.ndarray of 3*shape of uint8) converted image in RGB with the same dimension """ # TODO: handle signed values assert (len(data.shape) == 2) # => 2D with greyscale # fit it to 8 bits and update brightness and contrast at the same time if irange is None: # automatic scaling (not so fast as min and max must be found) # drescaled = scipy.misc.bytescale(data) irange = (data.view(numpy.ndarray).min(), data.view(numpy.ndarray).max()) if data.dtype == "uint8" and irange == (0, 255): # short-cut when data is already the same type # logging.debug("Applying direct range mapping to RGB") drescaled = data # TODO: also write short-cut for 16 bits by reading only the high byte? else: # If data might go outside of the range, clip first if data.dtype.kind in "iu": # no need to clip if irange is the whole possible range idt = numpy.iinfo(data.dtype) # trick to ensure B&W if there is only one value allowed if irange[0] >= irange[1]: if irange[0] > idt.min: irange = [irange[1] - 1, irange[1]] else: irange = [irange[0], irange[0] + 1] if img_fast: try: # only (currently) supports uint16 return img_fast.DataArray2RGB(data, irange, tint) except ValueError as exp: logging.debug("Fast conversion cannot run: %s", exp) except Exception: logging.exception("Failed to use the fast conversion") if irange[0] > idt.min or irange[1] < idt.max: data = data.clip(*irange) else: # floats et al. => always clip # TODO: might not work correctly if range is in middle of data # values trick to ensure B&W image if irange[0] >= irange[1] and irange[0] > float(data.min()): force_white = True else: force_white = False # img_fast currently doesn't support floats data = data.clip(*irange) if force_white: irange = [irange[1] - 1, irange[1]] drescaled = scipy.misc.bytescale(data, cmin=irange[0], cmax=irange[1]) # Now duplicate it 3 times to make it rgb (as a simple approximation of # greyscale) # dstack doesn't work because it doesn't generate in C order (uses strides) # apparently this is as fast (or even a bit better): # 0 copy (1 malloc) rgb = numpy.empty(data.shape + (3, ), dtype="uint8", order='C') # Tint (colouration) if tint == (255, 255, 255): # fast path when no tint # Note: it seems numpy.repeat() is 10x slower ?! # a = numpy.repeat(drescaled, 3) # a.shape = data.shape + (3,) rgb[:, :, 0] = drescaled # 1 copy rgb[:, :, 1] = drescaled # 1 copy rgb[:, :, 2] = drescaled # 1 copy else: rtint, gtint, btint = tint # multiply by a float, cast back to type of out, and put into out array # TODO: multiplying by float(x/255) is the same as multiplying by int(x) # and >> 8 numpy.multiply(drescaled, rtint / 255, out=rgb[:, :, 0]) numpy.multiply(drescaled, gtint / 255, out=rgb[:, :, 1]) numpy.multiply(drescaled, btint / 255, out=rgb[:, :, 2]) return rgb