У меня есть функция, которую я буду называть "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
Полный пример рабочего кода того, что я пытаюсь достичь, я использую 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)
Существует несколько простых и очень эффективных оптимизаций:
(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
Во-первых, добавьте, что Consts
находится в вашей функции rgb2something
, так как это поможет нам понять, что именно делает функция.
Лучшим способом ускорить это было бы векторизация операции.
Вам не нужно создавать таблицу поиска для этой операции. Если у вас есть функция, применяемая к каждому вектору (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
(которая является последней осью канала).
Хотя кэширование не является необходимым, могут быть конкретные случаи использования, когда это принесет вам большую пользу. Возможно, вы хотите выполнить эту пиксельную операцию 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)
Возможно, ваши изображения достаточно разнообразны, чтобы охватить большой набор всего диапазона поиска, а стоимость построения всего кеша может быть амортизирована за счет уменьшенной стоимости поиска.
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)
rgb2something
? | Да, генерация и использование справочной таблицы на 64 или 128 МБ не будет очень эффективной (генерация составляет 2 ^ 24 итерации в Python - интерпретатор медленный - и большой поиск не будет очень удобен для кэша). Векторизованный подход (с хорошим, предсказуемым шаблоном доступа) был бы намного лучше, как упоминалось в приведенном выше комментарии.