Создание быстрых таблиц поиска RGB в Python

1

У меня есть функция, которую я буду называть "rgb2something", которая преобразует данные RGB [1x1x3] в одно значение (вероятность), зацикливание по каждому пикселю во входных данных RGB оказывается довольно медленным.

Я попробовал следующий подход, чтобы ускорить преобразование. Чтобы создать LUT (Look up table):

import numpy as np

levels = 256
levels2 = levels**2
lut = [0] * (levels ** 3)

levels_range = range(0, levels)

for r in levels_range:
    for g in levels_range:
        for b in levels_range:
            lut[r + (g * levels) + (b * levels2)] = rgb2something(r, g, b)

И для преобразования RGB в преобразованное изображение вероятности:

result = np.take(lut, r_channel + (g_channel * 256) + (b_channel * 65536))

Однако как генерация LUT, так и вычисление результата все еще медленны. В 2-х измерениях он довольно быстро, однако в 3-х измерениях (r, g и b) он замедляется. Как я могу увеличить производительность?

РЕДАКТИРОВАТЬ

rgb2something(r, g, b) выглядит так:

def rgb2something(r, g, b):
    y = np.array([[r, g, b]])
    y_mean = np.mean(y, axis=0)
    y_centered = y - y_mean
    y_cov = y_centered.T.dot(y_centered) / len(y_centered)
    m = len(Consts.x)
    n = len(y)
    q = m + n
    pool_cov = (m / q * x_cov) + (n / q * y_cov)
    inv_pool_cov = np.linalg.inv(pool_cov)
    g = Consts.x_mean - y_mean
    mah = g.T.dot(inv_pool_cov).dot(g) ** 0.5
    return mah

EDIT 2:

Полный пример рабочего кода того, что я пытаюсь достичь, я использую OpenCV, поэтому любые подходы OpenCV, такие как Apply LUT, приветствуются, как и подходы C/C++:

import matplotlib.pyplot as plt
import numpy as np 
import cv2

class Model:
    x = np.array([
        [6, 5, 2],
        [2, 5, 7],
        [6, 3, 1]
    ])
    x_mean = np.mean(x, axis=0)
    x_centered = x - x_mean
    x_covariance = x_centered.T.dot(x_centered) / len(x_centered)
    m = len(x)
    n = 1  # Only ever comparing to a single pixel
    q = m + n
    pooled_covariance = (m / q * x_covariance)  # + (n / q * y_cov) -< Always 0 for a single point
    inverse_pooled_covariance = np.linalg.inv(pooled_covariance)

def rgb2something(r, g, b):
    #Calculates Mahalanobis Distance between pixel and model X
    y = np.array([[r, g, b]])
    y_mean = np.mean(y, axis=0)
    g = Model.x_mean - y_mean
    mah = g.T.dot(Model.inverse_pooled_covariance).dot(g) ** 0.5
    return mah

def generate_lut():
    levels = 256
    levels2 = levels**2
    lut = [0] * (levels ** 3)

    levels_range = range(0, levels)

    for r in levels_range:
        for g in levels_range:
            for b in levels_range:
                lut[r + (g * levels) + (b * levels2)] = rgb2something(r, g, b)

    return lut

def calculate_distance(lut, input_image):
    return np.take(lut, input_image[:, :, 0] + (input_image[:, :, 1] * 256) + (input_image[:, :, 2] * 65536))

lut = generate_lut()
rgb = np.random.randint(255, size=(1080, 1920, 3), dtype=np.uint8)
result = calculate_distance(lut, rgb)

cv2.imshow("Example", rgb)
cv2.imshow("Result", result)
cv2.waitKey(0)
  • 2
    Вы пробовали NumPy Vectorise с комбинациями?
  • 1
    Как выглядит rgb2something ? | Да, генерация и использование справочной таблицы на 64 или 128 МБ не будет очень эффективной (генерация составляет 2 ^ 24 итерации в Python - интерпретатор медленный - и большой поиск не будет очень удобен для кэша). Векторизованный подход (с хорошим, предсказуемым шаблоном доступа) был бы намного лучше, как упоминалось в приведенном выше комментарии.
Показать ещё 7 комментариев
Теги:
opencv
numpy
performance
lookup-tables

2 ответа

3
Лучший ответ

Обновление: добавлена оптимизация blas

Существует несколько простых и очень эффективных оптимизаций:

(1) векторизация, векторизация! В этом коде не так сложно вкратце выделить все, что угодно. Увидеть ниже.

(2) использовать правильный поиск, то есть причудливое индексирование, а не np.take

(3) использовать Cholesky decomp. С blas dtrmm мы можем использовать треугольную структуру

И вот код. Просто добавьте его в конец кода OP (в разделе EDIT 2). Если вы не очень терпеливы, вы, вероятно, также захотите прокомментировать lut = generate_lut() и result = calculate_distance(lut, rgb) и все ссылки на cv2. Я также добавил случайную строку в x чтобы сделать ее ковариационную матрицу несингулярной.

class Full_Model(Model):
    ch = np.linalg.cholesky(Model.inverse_pooled_covariance)
    chx = Model.x_mean@ch

def rgb2something_vectorized(rgb):
    return np.sqrt(np.sum(((rgb - Full_Model.x_mean)@Full_Model.ch)**2,  axis=-1))

from scipy.linalg import blas

def rgb2something_blas(rgb):
    *shp, nchan = rgb.shape
    return np.sqrt(np.einsum('...i,...i', *2*(blas.dtrmm(1, Full_Model.ch.T, rgb.reshape(-1, nchan).T, 0, 0, 0, 0, 0).T - Full_Model.chx,))).reshape(shp)

def generate_lut_vectorized():
    return rgb2something_vectorized(np.transpose(np.indices((256, 256, 256))))

def generate_lut_blas():
    rng = np.arange(256)
    arr = np.empty((256, 256, 256, 3))
    arr[0, ..., 0]  = rng
    arr[0, ..., 1]  = rng[:, None]
    arr[1:, ...] = arr[0]
    arr[..., 2] = rng[:, None, None]
    return rgb2something_blas(arr)

def calculate_distance_vectorized(lut, input_image):
    return lut[input_image[..., 2], input_image[..., 1], input_image[..., 0]]

# test code

def random_check_lut(lut):
    """Because the original lut generator is excruciatingly slow,
    we only compare a random sample, using the original code
    """
    levels = 256
    levels2 = levels**2
    lut = lut.ravel()

    levels_range = range(0, levels)

    for r, g, b in np.random.randint(0, 256, (1000, 3)):
        assert np.isclose(lut[r + (g * levels) + (b * levels2)], rgb2something(r, g, b))

import time
td = []
td.append((time.time(), 'create lut vectorized'))
lutv = generate_lut_vectorized()
td.append((time.time(), 'create lut using blas'))
lutb = generate_lut_blas()
td.append((time.time(), 'lookup using np.take'))
res = calculate_distance(lutv, rgb)
td.append((time.time(), 'process on the fly (no lookup)'))
resotf = rgb2something_vectorized(rgb)
td.append((time.time(), 'process on the fly (blas)'))
resbla = rgb2something_blas(rgb)
td.append((time.time(), 'lookup using fancy indexing'))
resv = calculate_distance_vectorized(lutv, rgb)
td.append((time.time(), None))

print("sanity checks ... ", end='')
assert np.allclose(res, resotf) and np.allclose(res, resv) \
    and np.allclose(res, resbla) and np.allclose(lutv, lutb)
random_check_lut(lutv)
print('all ok\n')

t, d = zip(*td)
for ti, di in zip(np.diff(t), d):
    print(f'{di:32s} {ti:10.3f} seconds')

Пример прогона:

sanity checks ... all ok

create lut vectorized                 1.116 seconds
create lut using blas                 0.917 seconds
lookup using np.take                  0.398 seconds
process on the fly (no lookup)        0.127 seconds
process on the fly (blas)             0.069 seconds
lookup using fancy indexing           0.064 seconds

Мы можем видеть, что лучший поиск превосходит лучшие на лету вычисления усами. При этом пример может переоценить стоимость поиска, поскольку случайные пиксели, по-видимому, менее дружественны к кешу, чем естественные изображения.

Оригинальный ответ (возможно, еще полезный для некоторых)

Если rgb2something не может быть векторизован, и вы хотите обработать одно типичное изображение, то вы можете получить приличное ускорение, используя np.unique.

Если rgb2something дорого и несколько изображений должны быть обработаны, то unique может быть объединен с кэшированием, что удобно сделать с помощью functools.lru_cache ---only (неосновной) камень преткновения: аргументы должны быть hashable. Как оказалось, модификация в коде, что эти силы (литье rgb-массивов в 3-байтовые строки), приносит пользу производительности.

Использование полной таблицы поиска стоит того, если у вас есть огромное количество пикселей, охватывающих большинство оттенков. В этом случае самый быстрый способ - использовать numping fancy indexing для выполнения фактического поиска.

import numpy as np
import time
import functools

def rgb2something(rgb):
    # waste some time:
    np.exp(0.1*rgb)
    return rgb.mean()

@functools.lru_cache(None)
def rgb2something_lru(rgb):
    rgb = np.frombuffer(rgb, np.uint8)
    # waste some time:
    np.exp(0.1*rgb)
    return rgb.mean()

def apply_to_img(img):
    shp = img.shape
    return np.reshape([rgb2something(x) for x in img.reshape(-1, shp[-1])], shp[:2])

def apply_to_img_lru(img):
    shp = img.shape
    return np.reshape([rgb2something_lru(x) for x in img.ravel().view('S3')], shp[:2])

def apply_to_img_smart(img, print_stats=True):
    shp = img.shape
    unq, bck = np.unique(img.reshape(-1, shp[-1]), return_inverse=True, axis=0)
    if print_stats:
        print('total no pixels', shp[0]*shp[1], '\nno unique pixels', len(unq))
    return np.array([rgb2something(x) for x in unq])[bck].reshape(shp[:2])

def apply_to_img_smarter(img, print_stats=True):
    shp = img.shape
    unq, bck = np.unique(img.ravel().view('S3'), return_inverse=True)
    if print_stats:
        print('total no pixels', shp[0]*shp[1], '\nno unique pixels', len(unq))
    return np.array([rgb2something_lru(x) for x in unq])[bck].reshape(shp[:2])

def make_full_lut():
    x = np.empty((3,), np.uint8)
    return np.reshape([rgb2something(x) for x[0] in range(256)
                       for x[1] in range(256) for x[2] in range(256)],
                      (256, 256, 256))

def make_full_lut_cheat(): # for quicker testing lookup
    i, j, k = np.ogrid[:256, :256, :256]
    return (i + j + k) / 3

def apply_to_img_full_lut(img, lut):
    return lut[(*np.moveaxis(img, 2, 0),)]

from scipy.misc import face

t0 = time.perf_counter()
bw = apply_to_img(face())
t1 = time.perf_counter()
print('naive                 ', t1-t0, 'seconds')

t0 = time.perf_counter()
bw = apply_to_img_lru(face())
t1 = time.perf_counter()
print('lru first time        ', t1-t0, 'seconds')

t0 = time.perf_counter()
bw = apply_to_img_lru(face())
t1 = time.perf_counter()
print('lru second time       ', t1-t0, 'seconds')

t0 = time.perf_counter()
bw = apply_to_img_smart(face(), False)
t1 = time.perf_counter()
print('using unique:         ', t1-t0, 'seconds')

rgb2something_lru.cache_clear()

t0 = time.perf_counter()
bw = apply_to_img_smarter(face(), False)
t1 = time.perf_counter()
print('unique and lru first: ', t1-t0, 'seconds')

t0 = time.perf_counter()
bw = apply_to_img_smarter(face(), False)
t1 = time.perf_counter()
print('unique and lru second:', t1-t0, 'seconds')

t0 = time.perf_counter()
lut = make_full_lut_cheat()
t1 = time.perf_counter()
print('creating full lut:    ', t1-t0, 'seconds')

t0 = time.perf_counter()
bw = apply_to_img_full_lut(face(), lut)
t1 = time.perf_counter()
print('using full lut:       ', t1-t0, 'seconds')

print()
apply_to_img_smart(face())

import Image
Image.fromarray(bw.astype(np.uint8)).save('bw.png')

Пример прогона:

naive                  6.8886632949870545 seconds
lru first time         1.7458112589956727 seconds
lru second time        0.4085628940083552 seconds
using unique:          2.0951434450107627 seconds
unique and lru first:  2.0168916099937633 seconds
unique and lru second: 0.3118703299842309 seconds
creating full lut:     151.17599205300212 seconds
using full lut:        0.12164952099556103 seconds

total no pixels 786432 
no unique pixels 134105
  • 0
    Спасибо за ваши тесты, однако, уже известно, что полная LUT быстрее, вопрос в том, как оптимизировать эту проблему, создав либо меньшую LUT, либо более быстрый шаблон доступа для большей LUT. Поскольку это будет система реального времени, вероятен большой диапазон оттенков.
  • 1
    @RaymondTunstill « уже известно, что полное LUT быстрее ». Ну, может быть, но вы только что поделились некоторыми важными частями информации. Во всяком случае, я подозреваю, что вы найдете обновленный ответ интересным.
3

Во-первых, добавьте, что Consts находится в вашей функции rgb2something, так как это поможет нам понять, что именно делает функция.

Лучшим способом ускорить это было бы векторизация операции.

1) Отсутствие кеширования

Вам не нужно создавать таблицу поиска для этой операции. Если у вас есть функция, применяемая к каждому вектору (r, g, b), вы можете просто применить его для каждого вектора изображения, используя np.apply_along_axis. В следующем примере я предполагаю простое определение для rgb2something как заполнителя - эта функция, конечно, может быть заменена вашим определением.

def rgb2something(vector):
    return sum(vector)

image = np.random.randint(0, 256, size=(100, 100, 3), dtype=np.uint8)
transform = np.apply_along_axis(rgb2something, -1, image)

Это берет массив image и применяет функцию rgb2something к каждому 1-D фрагменту вдоль оси -1 (которая является последней осью канала).

2) Лицевая таблица поиска

Хотя кэширование не является необходимым, могут быть конкретные случаи использования, когда это принесет вам большую пользу. Возможно, вы хотите выполнить эту пиксельную операцию rgb2something через тысячи изображений, и вы подозреваете, что многие пиксельные значения будут повторяться через изображения. В таких случаях построение таблицы поиска может значительно повысить производительность. Я бы предложил лениво заполнить таблицу (я предполагаю, что это предполагает, что ваш набор данных охватывает изображения, которые несколько похожи - с похожими объектами, текстурами и т.д., Что означало бы, что в целом они охватывают только относительно небольшое подмножество всего 2 ^ 24 место поиска). Если вы чувствуете, что они охватывают относительно большое подмножество, вы можете заранее составить всю таблицу поиска (см. Следующий раздел).

lut = [-1] * (256 ** 3)

def actual_rgb2something(vector):
    return sum(vector)

def rgb2something(vector):
    value = lut[vector[0] + vector[1] * 256 + vector[2] * 65536]

    if value == -1:
        value = actual_rgb2something(vector)
        lut[vector[0] + vector[1] * 256 + vector[2] * 65536] = value

    return value

Затем вы можете преобразовать каждое изображение так же, как и раньше:

image = np.random.randint(0, 256, size=(100, 100, 3), dtype=np.uint8)
transform = np.apply_along_axis(rgb2something, -1, image)

3) Предварительно вычисленный кеш

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

from itertools import product

lut = [-1] * (256 ** 3)

def actual_rgb2something(vector):
    return sum(vector)

def fill(vector):
    value = actual_rgb2something(vector)
    lut[vector[0] + vector[1] * 256 + vector[2] * 65536] = value

# Fill the table
total = list(product(range(256), repeat=3))
np.apply_along_axis(fill, arr=total, axis=1)

Теперь вместо того, чтобы вычислять значения снова, вы можете просто просмотреть их из таблицы:

def rgb2something(vector):
    return lut[vector[0] + vector[1] * 256 + vector[2] * 65536]

Преобразование изображений, конечно, такое же, как и раньше:

image = np.random.randint(0, 256, size=(100, 100, 3), dtype=np.uint8)
transform = np.apply_along_axis(rgb2something, -1, image)
  • 1
    Я считаю, что опер хочет улучшить его производительность за изображение, а не за каждое изображение
  • 0
    Кроме того, его код работает для каждой возможной комбинации векторов RGB, а не только вдоль вектора RGB изображения, т.е. вы заблокировали свою операцию вдоль оси, пока он выполняет цикл for вдоль всех трех своих осей.
Показать ещё 3 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню