Использование шагов для эффективного фильтра скользящей средней
Недавно я узнал о strides в ответе на этот пост, и задавался вопросом, как я могу использовать их для вычисления фильтра скользящей средней более эффективно, чем то, что я предложил в этом сообщении (используя фильтры свертки).
Это то, что у меня есть до сих пор. Он принимает вид исходного массива, затем свертывает его на необходимую сумму и суммирует значения ядра для вычисления среднего значения. Я знаю, что края не обрабатываются правильно, но я могу позаботиться об этом позже... Есть ли лучший и быстрый способ? Целью является фильтрация больших массивов с плавающей запятой размером до 5000x5000 x 16, задача, которая scipy.ndimage.filters.convolve
довольно медленная.
Обратите внимание, что я ищу 8-соседнюю связь, то есть фильтр 3x3 принимает среднее значение 9 пикселей (8 вокруг фокального пикселя) и присваивает это значение пикселю в новом изображении.
import numpy, scipy
filtsize = 3
a = numpy.arange(100).reshape((10,10))
b = numpy.lib.stride_tricks.as_strided(a, shape=(a.size,filtsize), strides=(a.itemsize, a.itemsize))
for i in range(0, filtsize-1):
if i > 0:
b += numpy.roll(b, -(pow(filtsize,2)+1)*i, 0)
filtered = (numpy.sum(b, 1) / pow(filtsize,2)).reshape((a.shape[0],a.shape[1]))
scipy.misc.imsave("average.jpg", filtered)
ИЗМЕНИТЬ Разъяснение того, как я вижу это:
Текущий код:
- используйте stride_tricks для создания массива типа [[0,1,2], [1,2,3], [2,3,4]...], который соответствует верхней строке ядра фильтра.
- Сверните по вертикальной оси, чтобы получить среднюю строку ядра [[10,11,12], [11,12,13], [13,14,15]...] и добавить ее в массив Я попал в 1)
- Повторите, чтобы получить нижнюю строку ядра [[20,21,22], [21,22,23], [22,23,24]...]. В этот момент я беру сумму каждой строки и деля ее на количество элементов в фильтре, давая мне среднее значение для каждого пикселя (сдвинутое на 1 строку и 1 столбец и с некоторыми нечеткими границами по краям, но я могу позаботьтесь об этом позже).
Я надеялся, что лучше использовать stride_tricks, чтобы получить 9 значений или сумму элементов ядра напрямую, для всего массива или что кто-то может убедить меня в еще одном более эффективном методе...
Ответы
Ответ 1
Для чего это стоит, вот как бы вы это сделали, используя "причудливые" шагающие трюки. Я собирался опубликовать это вчера, но отвлекся от реальной работы!:)
@Paul и @eat имеют хорошие реализации, используя различные другие способы сделать это. Чтобы продолжить работу по более раннему вопросу, я решил, что опубликую N-мерный эквивалент.
Однако вы не сможете значительно превзойти функции scipy.ndimage
для > 1D массивов. (scipy.ndimage.uniform_filter
должен бить scipy.ndimage.convolve
, хотя)
Кроме того, если вы пытаетесь получить многомерное движущееся окно, вы рискуете повредить память, когда вы непреднамеренно создаете копию своего массива. В то время как начальный "катящийся" массив - это просто представление в память вашего исходного массива, любые промежуточные шаги, которые копируют массив, сделают копию, которая на порядок больше, чем ваш исходный массив (например, предположим, что вы работаете с исходный массив 100x100... Представление в нем (для размера фильтра (3,3)) будет 98x98x3x3, но использует ту же память, что и оригинал. Однако любые копии будут использовать объем памяти, который будет иметь полный массив 98x98x3x3 будет!!)
В принципе, использование сумасшедших шагающих трюков отлично подходит для того, чтобы вы хотите векторизовать операции перемещения окна на одной оси ndarray. Это позволяет легко вычислить такие вещи, как перемещение стандартного отклонения и т.д. С очень небольшими накладными расходами. Когда вы хотите начать делать это по нескольким осям, это возможно, но вам обычно лучше работать с более специализированными функциями. (Например, scipy.ndimage
и т.д.)
Во всяком случае, вот как вы это делаете:
import numpy as np
def rolling_window_lastaxis(a, window):
"""Directly taken from Erik Rigtorp post to numpy-discussion.
<http://www.mail-archive.com/[email protected]/msg29450.html>"""
if window < 1:
raise ValueError, "`window` must be at least 1."
if window > a.shape[-1]:
raise ValueError, "`window` is too long."
shape = a.shape[:-1] + (a.shape[-1] - window + 1, window)
strides = a.strides + (a.strides[-1],)
return np.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)
def rolling_window(a, window):
if not hasattr(window, '__iter__'):
return rolling_window_lastaxis(a, window)
for i, win in enumerate(window):
if win > 1:
a = a.swapaxes(i, -1)
a = rolling_window_lastaxis(a, win)
a = a.swapaxes(-2, i)
return a
filtsize = (3, 3)
a = np.zeros((10,10), dtype=np.float)
a[5:7,5] = 1
b = rolling_window(a, filtsize)
blurred = b.mean(axis=-1).mean(axis=-1)
Итак, что мы получаем, когда делаем b = rolling_window(a, filtsize)
, это массив 8x8x3x3, который фактически представляет собой представление в ту же память, что и исходный массив 10x10. Мы могли бы так же легко использовать различные размеры фильтра по разным осям или работать только по выбранным осям N-мерного массива (т.е. filtsize = (0,3,0,3)
на 4-мерном массиве давали бы нам 6-мерное представление).
Затем мы можем применить произвольную функцию к последней оси, чтобы эффективно вычислять вещи в движущемся окне.
Однако, поскольку мы храним временные массивы, которые намного больше, чем наш исходный массив на каждом шаге mean
(или std
или что-то еще), это не совсем эффективная память! Это также не будет ужасно быстрым.
Эквивалент для ndimage
справедлив:
blurred = scipy.ndimage.uniform_filter(a, filtsize, output=a)
Это будет обрабатывать различные граничные условия, выполнять "размытие" на месте, не требуя временной копии массива, и быть очень быстрым. Уловки - хороший способ применить функцию к движущемуся окну вдоль одной оси, но они не являются хорошим способом сделать это по нескольким осям, обычно....
Просто мои $0,02, во всяком случае...
Ответ 2
Я недостаточно хорошо знаком с Python, чтобы выписать код для этого, но два лучших способа ускорить свертки - либо отделить фильтр, либо использовать преобразование Фурье.
Отдельный фильтр: свертка - это O (M * N), где M и N - количество пикселей на изображении и в фильтре, соответственно. Поскольку средняя фильтрация с ядром 3 на 3 эквивалентна фильтрации сначала с ядром 3 на 1, а затем с ядром 1 на 3, вы можете получить улучшение скорости (3+3)/(3*3)
= ~ 30% путем последовательной свертки с два 1-ядерных ядра (это, очевидно, улучшается по мере увеличения ядра). Конечно, вы все равно можете использовать трюки с шагами.
Преобразование Фурье: conv(A,B)
эквивалентно ifft(fft(A)*fft(B))
, т.е. свертка в прямом пространстве становится умножением в пространстве Фурье, где A
- ваше изображение, а B
- ваш фильтр. Так как умножение (преобразование по элементу) преобразований Фурье требует, чтобы A и B были одного размера, B представляет собой массив size(A)
с вашим ядром в самом центре изображения и нулями всюду. Чтобы поместить ядро 3 на 3 в центр массива, вам может потребоваться добавить A
к нечетному размеру. В зависимости от реализации преобразования Фурье это может быть намного быстрее, чем свертка (и если вы применяете один и тот же фильтр несколько раз, вы можете предварительно вычислить fft(B)
, сохранив еще 30% времени вычисления).
Ответ 3
Одна вещь, которую я уверен, должна быть исправлена, - это ваш массив представлений b
.
У него есть несколько элементов из нераспределенной памяти, поэтому вы получите сбои.
Учитывая ваше новое описание вашего алгоритма, первое, что нужно исправить, - это тот факт, что вы выходите за пределы выделения a
:
bshape = (a.size-filtsize+1, filtsize)
bstrides = (a.itemsize, a.itemsize)
b = numpy.lib.stride_tricks.as_strided(a, shape=bshape, strides=bstrides)
Обновление
Потому что я все еще не совсем понимаю метод, и, кажется, есть более простые способы решения проблемы, я просто собираюсь сделать это здесь:
A = numpy.arange(100).reshape((10,10))
shifts = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]
B = A[1:-1, 1:-1].copy()
for dx,dy in shifts:
xstop = -1+dx or None
ystop = -1+dy or None
B += A[1+dx:xstop, 1+dy:ystop]
B /= 9
... который кажется просто прямым подходом. Единственная посторонняя операция заключается в том, что она распределяет и заполняет b
только один раз. Все добавление, деление и индексация должны выполняться независимо. Если вы делаете 16 групп, вам все равно нужно выделить b
один раз, если вы намерены сохранить изображение. Даже если это не поможет, это может прояснить, почему я не понимаю проблему или, по крайней мере, служит отправной точкой для ускорения других методов. Это работает в 2,6 секунды на моем ноутбуке на 5k x 5k массиве float64, из которых 0,5 - создание b
Ответ 4
Давайте посмотрим:
Это не так ясно из вашего вопроса, но я предполагаю теперь, что вы захотите значительно улучшить этот вид усреднения.
import numpy as np
from numpy.lib import stride_tricks as st
def mf(A, k_shape= (3, 3)):
m= A.shape[0]- 2
n= A.shape[1]- 2
strides= A.strides+ A.strides
new_shape= (m, n, k_shape[0], k_shape[1])
A= st.as_strided(A, shape= new_shape, strides= strides)
return np.sum(np.sum(A, -1), -1)/ np.prod(k_shape)
if __name__ == '__main__':
A= np.arange(100).reshape((10, 10))
print mf(A)
Теперь, какие улучшения производительности вы действительно ожидаете?
Update:
Прежде всего, предупреждение: код в этом текущем состоянии неправильно адаптируется к форме "ядро". Однако это не моя главная проблема прямо сейчас (во всяком случае, идея уже есть, как правильно адаптироваться).
Я только что выбрал новую форму 4D A интуитивно, для меня действительно имеет смысл подумать о том, чтобы центр 2D-ядра был центрирован для каждой позиции сетки исходного 2D A.
Но это 4D-формирование не может быть "лучшим". Я думаю, что настоящая проблема здесь - выполнение суммирования. Нужно уметь находить "лучший заказ" (4D A), чтобы полностью использовать архитектуру кэша вашей машины. Однако этот порядок не может быть одинаковым для "малых" массивов, которые "взаимодействуют" с кешем вашей машины и с теми большими, которые не имеют (по крайней мере, не так прямолинейно).
Обновление 2:
Вот немного измененная версия mf
. Ясно, что лучше сначала преобразовать в 3D-массив, а затем вместо суммирования просто сделать точечный продукт (у этого есть преимущество, поэтому ядро может быть произвольным). Однако он все еще на 3 раза медленнее (на моей машине), чем обновленная функция Pauls.
def mf(A):
k_shape= (3, 3)
k= np.prod(k_shape)
m= A.shape[0]- 2
n= A.shape[1]- 2
strides= A.strides* 2
new_shape= (m, n)+ k_shape
A= st.as_strided(A, shape= new_shape, strides= strides)
w= np.ones(k)/ k
return np.dot(A.reshape((m, n, -1)), w)