Ускорение "for-loop" при анализе изображений при итерациях до 40 000

Детали предварительных условий этого кода довольно длинные, поэтому я постараюсь изо всех сил подвести итог. WB/RG/BYColor - это базовое изображение, FIDO - это наложение этого базового изображения, которое применяется к нему. S_wb/rg/by - окончательные выходные изображения. WB/RG/BYColor имеют тот же размер, что и FIDO.

Для каждого уникального элемента в FIDO мы хотим рассчитать средний цвет этой области в базовых изображениях. Код ниже делает это, но, поскольку numFIDOs очень велико (до 40 000), это занимает долгое время.

Средние значения вычисляются для трех отдельных каналов RGB.

sX=200
sY=200
S_wb = np.zeros((sX, sY))
S_rg = np.zeros((sX, sY))
S_by = np.zeros((sX, sY))
uniqueFIDOs, unique_counts = np.unique(FIDO, return_counts=True) 
numFIDOs = uniqueFIDOs.shape  
for i in np.arange(0,numFIDOs[0]):
    Lookup = FIDO==uniqueFIDOs[i]
    # Get average of color signals for this FIDO
    S_wb[Lookup] = np.sum(WBColor[Lookup])/unique_counts[i]
    S_rg[Lookup] = np.sum(RGColor[Lookup])/unique_counts[i]
    S_by[Lookup] = np.sum(BYColor[Lookup])/unique_counts[i]

Это займет около 7,89 секунд для запуска, не так долго, но это будет включено в другой цикл, поэтому он будет наращиваться!

Я пробовал векторизация (показано ниже), но я не мог этого сделать

FIDOsize = unique_counts[0:numFIDOs[0]:1]
Lookup = FIDO ==uniqueFIDOs[0:numFIDOs[0]:1]
S_wb[Lookup] = np.sum(WBColor[Lookup])/FIDOsize
S_rg[Lookup] = np.sum(RGColor[Lookup])/FIDOsize
S_by[Lookup] = np.sum(BYColor[Lookup])/FIDOsize

ошибка при сопоставлении размера массива

Ответ 1

Это уже реализовано в Scipy, поэтому вы можете сделать:

from scipy.ndimage.measurements import mean as labeled_mean

labels = np.arange(FIDO.max()+1, dtype=int)
S_wb = labeled_mean(WBColor, FIDO, labels)[FIDO]
S_rg = labeled_mean(RGColor, FIDO, labels)[FIDO]
S_by = labeled_mean(BYColor, FIDO, labels)[FIDO]

Это предполагает, что FIDO содержит относительно небольшие целые числа. Если это не так, вы можете преобразовать его через np.unique(FIDO, return_inverse=True).

Этот простой код примерно в 1000 раз быстрее оригинала, для изображений 200х200 и FIDO, содержащих случайные целые числа от нуля до 40 000.

Ответ 2

По моим срокам это примерно в 10 раз быстрее, чем ваш оригинальный метод. Я тестировал эти массивы:

import numpy as np

sX=200
sY=200

FIDO = np.random.randint(0, sX*sY, (sX, sY))
WBColor = np.random.randint(0, sX*sY, (sX, sY))
RGColor = np.random.randint(0, sX*sY, (sX, sY))
BYColor = np.random.randint(0, sX*sY, (sX, sY))

Это часть, которую я приурочен:

import collections

colors = {'wb': WBColor, 'rg': RGColor, 'by': BYColor}
planes = colors.keys()
S = {plane: np.zeros((sX, sY)) for plane in planes}

for plane in planes:
    counts = collections.defaultdict(int)
    sums = collections.defaultdict(int)
    for (i, j), f in np.ndenumerate(FIDO):
        counts[f] += 1
        sums[f] += colors[plane][i, j]
    for (i, j), f in np.ndenumerate(FIDO):
        S[plane][i, j] = sums[f]/counts[f]

Вероятно, потому что хотя цикл в Python медленный, это меньше обходит данные.

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

Ответ 3

Ваш код не является оптимальным, потому что вы просматриваете все изображения для каждого региона в FIDO. Лучший подход - группировать пиксели каждой области и сначала вычислять средства. pandas дают хорошие инструменты для таких вычислений (только на одном канале здесь). Затем вы охватываете средства в регионах:

import numpy as np
import pandas as pd     
sX=200
sY=200
Nreg=sX*sY
WBColor=np.random.randint(0,256,(sX,sY))
FIDO=np.random.randint(0,Nreg,(sX,sY))


def oldloop():
    S_wb = np.zeros((sX, sY))
    uniqueFIDOs, unique_counts = np.unique(FIDO, return_counts=True) 
    numFIDOs = uniqueFIDOs.shape 
    for i in np.arange(0,numFIDOs[0]):
        Lookup = FIDO==uniqueFIDOs[i]
        S_wb[Lookup] = np.sum(WBColor[Lookup])/unique_counts[i]
    return S_wb

def newloop():
    index=pd.Index(FIDO.flatten(),name='region')
    means= pd.DataFrame(WBColor.flatten(),index).groupby(level='region').mean()
    lookup=np.zeros(Nreg)
    lookup[means.index]=means.values
    return lookup[FIDO]

в этом случае это примерно в 200 раз быстрее:

In [32]: np.allclose(oldloop(),newloop())
Out[32]: True

In [33]: %timeit -n1 oldloop()
1 loops, best of 3: 3.92 s per loop

In [34]: %timeit -n100 newloop()
100 loops, best of 3: 20.5 ms per loop    

ИЗМЕНИТЬ

Другой современный современный подход - использовать numba. Вы пишете (очень) базовый код python, работающий со скоростью C:

from numba import jit

@jit
def numbaloops():
    counts=np.zeros(Nreg)
    sums=np.zeros(Nreg)
    S = np.empty((sX, sY))
    for x in range(sX):
        for y in range(sY):
            region=FIDO[x,y]
            value=WBColor[x,y]
            counts[region]+=1
            sums[region]+=value
    for x in range(sX):
        for y in range(sY):
            region=FIDO[x,y]
            S[x,y]=sums[region]/counts[region]
    return S                

Теперь вы примерно в 4000 раз быстрее:

In [45]: np.allclose(oldloop(),numbaloops())
Out[45]: True

In [46]: %timeit -n1000 numbaloops()
1000 loops, best of 3: 1.06 ms per loop 

Ответ 4

Как и предлагалось раньше, код довольно сложный для векторизации. Его нельзя запускать параллельно, если вы не знаете, какие пиксели будут принадлежать каждому FIDO заранее. Я не знаю, вызываете ли вы FIDO для суперпикселей, но я обычно работаю с такими проблемами, и лучшее решение, которое я нашел до сих пор, выглядит следующим образом:

  • Сгладить данные:

    data = data.reshape(-1, 3)
    labels = FIDO.copy()
    

    Здесь data - ваш (Width, Height, 3) образ, а не отдельные 3 вектора, которые у вас есть. Он сглаживается до (Width * Height, 3).

  • Отмените FIDO до 0..N-1, где N= num unique FIDO:

    from skimage.segmentation import relabel_sequential
    
    labels = relabel_sequential(labels)[0]
    labels -= labels.min()
    

    Вышеизложенное из scikit-image преобразует ваш массив FIDO в диапазон [0, N-1], с которым гораздо легче работать позже.

  • Наконец, код в cython - простая функция для вычисления среднего значения для каждого из FIDO; s (поскольку они упорядочены от 0 до N, вы можете сделать это в 1D массиве с длиной N):

    def fmeans(double[:, ::1] data, long[::1] labels, long nsp):
        cdef long n,  N = labels.shape[0]
        cdef int K = data.shape[1]
        cdef double[:, ::1] F = np.zeros((nsp, K), np.float64)
        cdef int[::1] sizes = np.zeros(nsp, np.int32)
        cdef long l, b
        cdef double t
    
        for n in range(N):
            l = labels[n]
            sizes[l] += 1
    
            for z in range(K):
                t = data[n, z]
                F[l, z] += t
    
        for n in range(nsp):
            for z in range(K):
                F[n, z] /= sizes[n]
    
    return np.asarray(F)
    

Вы можете вызывать эту функцию позже (после компиляции с помощью cython), так же просто, как:

mean_colors = fmeans(data, labels.flatten(), labels.max()+1) # labels.max()+1 == N

Изображение средних цветов затем может быть восстановлено как:

mean_img = mean_colors[labels]

Если вы не хотите кодировать код на cython, scikit-image также предоставляет привязки для этого, используя структуру графа и networkx, однако гораздо медленнее:

http://scikit-image.org/docs/dev/auto_examples/plot_rag_mean_color.html

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

ПРИМЕЧАНИЕ: подход cython выполняется намного быстрее, вместо того, чтобы повторять число уникальных FIDO N и для каждого из них сканировать изображение (размер M = Width x Height), это только выполняет итерацию изображения ОДИН РАЗ. Таким образом, вычислительная стоимость находится в порядке O(M+N), а не O(M*N) вашего исходного подхода.


Пример теста:

import numpy as np
from skimage.segmentation import relabel_sequential

sX=200
sY=200

FIDO = np.random.randint(0, sX*sY, (sX, sY))
data = np.random.rand(sX, sY, 3) # Your image

Сгладить и отредактировать:

data = data.reshape(-1, 3)
labels = relabel_sequential(FIDO)[0]
labels -= labels.min()

Среднее значение:

>>> %timeit color_means = fmeans(data, labels.flatten(), labels.max()+1)
1000 loops, best of 3: 520 µs per loop

Требуется 0,5 мс (половина миллисекунды) для изображения 200х200 для:

print labels.max()+1 # --> 25787 unique FIDO
print color_means.shape # --> (25287, 3), the mean color of each FIDO

Вы можете восстановить изображение средних цветов с помощью интеллектуального индексации:

mean_image = color_means[labels]
print mean_image.shape # --> (200, 200, 3)

Я сомневаюсь, что вы можете получить эту скорость с помощью необработанных подходов python (или, по крайней мере, я не нашел, как).

Ответ 5

Короче: петли на питоне медленны. Вы должны сделать одно из следующего:

  • vectorize (вы пробовали это, но вы утверждаете, что "это не работает" ), что вы имеете в виду, но не работаете? Векторизация (если возможно) всегда работает
  • переключитесь на Cython и объявите значение iterator int

Оба вышеуказанных подхода основываются на преобразовании петли бутылочной шейки в C-петлю.