Использование numpy для эффективного преобразования 16-битных данных изображения в 8 бит для отображения с масштабированием интенсивности

Я часто конвертирую 16-битные изображения в оттенках серого в 8-битные данные изображения для отображения. Почти всегда полезно настроить минимальную и максимальную интенсивность отображения, чтобы выделить "интересные" части изображения.

Нижеприведенный код делает примерно то, что я хочу, но он уродлив и неэффективен и делает много промежуточных копий данных изображения. Как я могу достичь того же результата с минимальным объемом памяти и временем обработки?

import numpy

image_data = numpy.random.randint( #Realistic images would be much larger
    low=100, high=14000, size=(1, 5, 5)).astype(numpy.uint16)

display_min = 1000
display_max = 10000.0

print(image_data)
threshold_image = ((image_data.astype(float) - display_min) *
                   (image_data > display_min))
print(threshold_image)
scaled_image = (threshold_image * (255. / (display_max - display_min)))
scaled_image[scaled_image > 255] = 255
print(scaled_image)
display_this_image = scaled_image.astype(numpy.uint8)
print(display_this_image)

Ответ 1

Что вы делаете, это полутонирование вашего изображения.

Методы, предлагаемые другими, отлично работают, но они повторяют много дорогостоящих вычислений снова и снова. Поскольку в uint16 не более 65 536 различных значений, использование справочной таблицы (LUT) может значительно упростить процесс. И поскольку LUT мал, вам не нужно так беспокоиться о том, чтобы делать что-то на месте, или не создавать логические массивы. Следующий код повторно использует функцию Bi Rico для создания 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

Итак, есть ускорение x5, что неплохо, я думаю...

Ответ 2

Чтобы уменьшить использование памяти, сделайте отсечение на месте и не создавайте логические массивы.

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)

Если вы ограничиваете пределы отсечения как целочисленные значения, вы можете поочередно сделать это:

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)

Обратите внимание: что временный массив float все равно будет создан в последней строке до создания массива uint8.

Ответ 3

Я бы не стал бросать изображение в float, вы могли бы сделать что-то вроде:

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

Здесь необязательная копия изображения создается в его родном типе данных, а 8-битная копия - в последней строке.