Using numpy to efficiently convert 16-bit image data to 8 bit for display, with intensity scaling

前端 未结 5 1970
耶瑟儿~
耶瑟儿~ 2020-12-15 09:55

I frequently convert 16-bit grayscale image data to 8-bit image data for display. It\'s almost always useful to adjust the minimum and maximum display intensity to highlight

相关标签:
5条回答
  • 2020-12-15 10:05

    I would avoid casting the image to float, you could do something like:

    import numpy as np
    
    def display(image, display_min, display_max):
        # Here I set copy=True in order to ensure the original image is not
        # modified. If you don't mind modifying the original image, you can
        # set copy=False or skip this step.
        image = np.array(image, copy=True)
    
        image.clip(display_min, display_max, out=image)
        image -= display_min
        image //= (display_min - display_max + 1) / 256.
        image = image.astype(np.uint8)
        # Display image
    

    Here an optional copy of the image is made in it's native data type and an 8 bit copy is make on the last line.

    0 讨论(0)
  • 2020-12-15 10:06

    This is the answer I found on crossvalidated board in comments under this solution https://stats.stackexchange.com/a/70808/277040

    Basically for converting from uint16 to uint8 algorithm looks like this

    a = (255 - 0) / (65535 - 0)
    b = 255 - a * 65535
    newvalue = (a * img + b).astype(np.uint8)
    

    A generalized version would look like this

    def convert(img, target_type_min, target_type_max, target_type):
        imin = img.min()
        imax = img.max()
    
        a = (target_type_max - target_type_min) / (imax - imin)
        b = target_type_max - a * imax
        new_img = (a * img + b).astype(target_type)
        return new_img
    

    e.g.

    imgu8 = convert(img16u, 0, 255, np.uint8)

    0 讨论(0)
  • 2020-12-15 10:09

    To reduce memory usage, do the clipping in-place and avoid creating the boolean arrays.

    dataf = image_data.astype(float)
    numpy.clip(dataf, display_min, display_max, out=dataf)
    dataf -= display_min
    datab = ((255. / (display_max - display_min)) * dataf).astype(numpy.uint8)
    

    If you keep your clipping limits as integer values, you can alternately do this:

    numpy.clip(image_data, display_min, display_max, out=image_data)
    image_data-= display_min
    datab = numpy.empty_like(image_data)
    numpy.multiply(255. / (display_max - display_min), image_data, out=datab)
    

    Note: that a temporary float array will still be created in the last line before the uint8 array is created.

    0 讨论(0)
  • 2020-12-15 10:11

    What you are doing is halftoning your image.

    The methods proposed by others work great, but they are repeating a lot of expensive computations over and over again. Since in a uint16 there are at most 65,536 different values, using a look-up table (LUT) can streamline things a lot. And since the LUT is small, you don't have to worry that much about doing things in place, or not creating boolean arrays. The following code reuses Bi Rico's function to create the LUT:

    import numpy as np
    import timeit
    
    rows, cols = 768, 1024
    image = np.random.randint(100, 14000,
                                 size=(1, rows, cols)).astype(np.uint16)
    display_min = 1000
    display_max = 10000
    
    def display(image, display_min, display_max): # copied from Bi Rico
        # Here I set copy=True in order to ensure the original image is not
        # modified. If you don't mind modifying the original image, you can
        # set copy=False or skip this step.
        image = np.array(image, copy=True)
        image.clip(display_min, display_max, out=image)
        image -= display_min
        np.floor_divide(image, (display_max - display_min + 1) / 256,
                        out=image, casting='unsafe')
        return image.astype(np.uint8)
    
    def lut_display(image, display_min, display_max) :
        lut = np.arange(2**16, dtype='uint16')
        lut = display(lut, display_min, display_max)
        return np.take(lut, image)
    
    
    >>> np.all(display(image, display_min, display_max) ==
               lut_display(image, display_min, display_max))
    True
    >>> timeit.timeit('display(image, display_min, display_max)',
                      'from __main__ import display, image, display_min, display_max',
                       number=10)
    0.304813282062
    >>> timeit.timeit('lut_display(image, display_min, display_max)',
                      'from __main__ import lut_display, image, display_min, display_max',
                      number=10)
    0.0591987428298
    

    So there is a x5 speed-up, which is not a bad thing, I guess...

    0 讨论(0)
  • 2020-12-15 10:24

    I know this is an old tread, but we now have cupy with gpu acceleration. With cupy is always faster (both methods from Jaime great comment run at closer speed) with cupy.

    import numpy as np
    import cupy as cp
    import timeit
    rows, cols = 768, 1024
    image = np.random.randint(100, 14000,
                                 size=(1, rows, cols)).astype(np.uint16)
    display_min = 1000
    display_max = 10000
    
    def display(image, display_min, display_max): # copied from Bi Rico
        # Here I set copy=True in order to ensure the original image is not
        # modified. If you don't mind modifying the original image, you can
        # set copy=False or skip this step.
        image = np.array(image, copy=True)
        image.clip(display_min, display_max, out=image)
        image -= display_min
        np.floor_divide(image, (display_max - display_min + 1) / 256,
                        out=image, casting='unsafe')
        return image.astype(np.uint8)
    
    def lut_display(image, display_min, display_max) :
        lut = np.arange(2**16, dtype='uint16')
        lut = display(lut, display_min, display_max)
        return np.take(lut, image)
    
    
    def displaycp(image2, display_min, display_max): # copied from Bi Rico
        # Here I set copy=True in order to ensure the original image is not
        # modified. If you don't mind modifying the original image, you can
        # set copy=False or skip this step.
        image2 = cp.array(image2, copy=True)
        image2.clip(display_min, display_max, out=image2)
        image2 -= display_min
        cp.floor_divide(image2, (display_max - display_min + 1) / 256,
                        out=image2, casting='unsafe')
        return image2.astype(cp.uint8)
    
    def lut_displaycp(image2, display_min, display_max) :
        lut = cp.arange(2**16, dtype='uint16')
        lut = displaycp(lut, display_min, display_max)
        return cp.take(lut, image2)
    
    np.all(display(image, display_min, display_max) ==
               lut_display(image, display_min, display_max))
    
    imagecp = cp.asarray(image)
    type(imagecp)
    
    cp.all(displaycp(imagecp, display_min, display_max) ==
               lut_displaycp(imagecp, display_min, display_max))
    
    np.all(cp.asnumpy(displaycp(imagecp, display_min, display_max)) ==
              display(image, display_min, display_max))
    

    Timings

    timeit.timeit('display(image, display_min, display_max)',
                      'from __main__ import display, image, display_min, display_max',
                       number=100)
    

    1.2715457340000285

    timeit.timeit('lut_display(image, display_min, display_max)',
                      'from __main__ import lut_display, image, display_min, display_max',
                      number=100)
    

    0.27357000399933895

    timeit.timeit('displaycp(imagecp, display_min, display_max)',
                      'from __main__ import displaycp, imagecp, display_min, display_max',
                       number=100)
    

    0.018452465999871492

    timeit.timeit('lut_displaycp(imagecp, display_min, display_max)',
                      'from __main__ import lut_displaycp, imagecp, display_min, display_max',
                      number=100)
    

    0.015030614999886893

    0 讨论(0)
提交回复
热议问题