Давайте сначала устраним несколько очевидных проблем - foreground.isOpened()
вернет true даже после того, как вы дойдете до конца видео, так что ваша программа в конечном итоге рухнет в этот момент. Решение двоякое. Прежде всего, протестируйте все 3 экземпляра VideoCapture
сразу после их создания, используя что-то вроде:
if not foreground.isOpened() or not background.isOpened() or not alpha.isOpened():
print "Unable to open input videos."
return
Это позволит убедиться, что все они открылись правильно. Следующая часть — это правильная обработка достижения конца видео. Это означает либо проверку первого из двух возвращаемых значений read()
, который является логическим флагом, представляющим успех, либо проверку того, является ли кадр None
.
while True:
r_fg, fr_foreground = foreground.read()
r_bg, fr_background = background.read()
r_a, fr_alpha = alpha.read()
if not r_fg or not r_bg or not r_a:
break # End of video
Кроме того, кажется, что вы на самом деле не вызываете cv2.destroyAllWindows()
- ()
отсутствует. Не то, чтобы это действительно имело значение.
Чтобы помочь исследовать и оптимизировать это, я добавил некоторые подробные тайминги, используя модуль timeit
и пару удобных функций.
from timeit import default_timer as timer
def update_times(times, total_times):
for i in range(len(times) - 1):
total_times[i] += (times[i+1]-times[i]) * 1000
def print_times(total_times, n):
print "Iterations: %d" % n
for i in range(len(total_times)):
print "Step %d: %0.4f ms" % (i, total_times[i] / n)
print "Total: %0.4f ms" % (np.sum(total_times) / n)
и изменил функцию main()
для измерения времени, затрачиваемого на каждый логический шаг — чтение, масштабирование, смешивание, показ, ожиданиеKey. Для этого я разбил деление на отдельные операторы. Я также сделал небольшую модификацию, которая заставляет это работать и в Python 2.x (/255
интерпретируется как целочисленное деление и дает неверные результаты).
times = [0.0] * 6
total_times = [0.0] * (len(times) - 1)
n = 0
while True:
times[0] = timer()
r_fg, fr_foreground = foreground.read()
r_bg, fr_background = background.read()
r_a, fr_alpha = alpha.read()
if not r_fg or not r_bg or not r_a:
break # End of video
times[1] = timer()
fr_foreground = fr_foreground / 255.0
fr_background = fr_background / 255.0
fr_alpha = fr_alpha / 255.0
times[2] = timer()
result = cmb(fr_foreground,fr_background,fr_alpha)
times[3] = timer()
cv2.imshow('My Image', result)
times[4] = timer()
if cv2.waitKey(1) == ord('q'): break
times[5] = timer()
update_times(times, total_times)
n += 1
print_times(total_times, n)
Когда я запускаю это с видео 1280x800 mp4 в качестве входных данных, я замечаю, что оно действительно довольно вялое и использует только 15% ЦП на моей 6-ядерной машине. Время работы секций следующее:
Iterations: 1190
Step 0: 11.4385 ms
Step 1: 37.1320 ms
Step 2: 39.4083 ms
Step 3: 2.5488 ms
Step 4: 10.7083 ms
Total: 101.2358 ms
Это говорит о том, что самыми большими узкими местами являются как этап масштабирования, так и этап смешивания. Низкая загрузка ЦП также неоптимальна, но давайте сначала сосредоточимся на легко висящих фруктах.
Давайте посмотрим на типы данных массивов numpy, которые мы используем. read()
дает нам массивы с dtype
из np.uint8
-- 8-битных целых чисел без знака. Однако деление с плавающей запятой (как написано) даст массив с dtype
из np.float64
-- 64-битных значений с плавающей запятой. На самом деле нам не нужен такой уровень точности для нашего алгоритма, поэтому нам лучше использовать только 32-битные числа с плавающей запятой — это будет означать, что если какая-либо из операций будет векторизована, мы потенциально можем выполнить в два раза больше вычислений в одном и том же алгоритме. количество времени.
Здесь есть два варианта. Мы могли бы просто привести делитель к np.float32
, что приведет к тому, что numpy выдаст нам результат с тем же dtype
:
fr_foreground = fr_foreground / np.float32(255.0)
fr_background = fr_background / np.float32(255.0)
fr_alpha = fr_alpha / np.float32(255.0)
Что дает нам следующие тайминги:
Iterations: 1786
Step 0: 9.2550 ms
Step 1: 19.0144 ms
Step 2: 21.2120 ms
Step 3: 1.4662 ms
Step 4: 10.8889 ms
Total: 61.8365 ms
Или мы могли бы сначала привести массив к np.float32
, а затем выполнить масштабирование на месте.
fr_foreground = np.float32(fr_foreground)
fr_background = np.float32(fr_background)
fr_alpha = np.float32(fr_alpha)
fr_foreground /= 255.0
fr_background /= 255.0
fr_alpha /= 255.0
Что дает следующие тайминги (разделение шага 1 на преобразование (1) и масштабирование (2) — остальные сдвигаются на 1):
Iterations: 1786
Step 0: 9.0589 ms
Step 1: 13.9614 ms
Step 2: 4.5960 ms
Step 3: 20.9279 ms
Step 4: 1.4631 ms
Step 5: 10.4396 ms
Total: 60.4469 ms
Оба примерно эквивалентны, работают примерно на 60% от исходного времени. Я буду придерживаться второго варианта, так как он пригодится на последующих этапах. Посмотрим, что еще мы можем улучшить.
Из предыдущих таймингов мы видим, что масштабирование больше не является узким местом, но идея все еще приходит на ум — деление, как правило, медленнее, чем умножение, так что, если мы умножим на обратное?
fr_foreground *= 1/255.0
fr_background *= 1/255.0
fr_alpha *= 1/255.0
Действительно, это дает нам миллисекунду — ничего впечатляющего, но это было легко, так что можно и с этим согласиться:
Iterations: 1786
Step 0: 9.1843 ms
Step 1: 14.2349 ms
Step 2: 3.5752 ms
Step 3: 21.0545 ms
Step 4: 1.4692 ms
Step 5: 10.6917 ms
Total: 60.2097 ms
Теперь функция смешивания является самым большим узким местом, за которым следует приведение типов всех трех массивов. Если мы посмотрим, что делает операция смешивания:
foreground * alpha + background * (1.0 - alpha)
мы можем заметить, что для того, чтобы математика работала, единственное значение, которое должно находиться в диапазоне (0,0, 1,0), — это alpha
.
Что, если мы масштабируем только альфа-изображение? Кроме того, поскольку умножение на число с плавающей запятой приведет к преобразованию в число с плавающей запятой, что, если мы также пропустим преобразование типа? Это означало бы, что cmb()
должен был бы вернуть массив np.uint8
def cmb(fg,bg,a):
return np.uint8(fg * a + bg * (1-a))
и у нас было бы
#fr_foreground = np.float32(fr_foreground)
#fr_background = np.float32(fr_background)
fr_alpha = np.float32(fr_alpha)
#fr_foreground *= 1/255.0
#fr_background *= 1/255.0
fr_alpha *= 1/255.0
Время для этого
Step 0: 7.7023 ms
Step 1: 4.6758 ms
Step 2: 1.1061 ms
Step 3: 27.3188 ms
Step 4: 0.4783 ms
Step 5: 9.0027 ms
Total: 50.2840 ms
Очевидно, что шаги 1 и 2 намного быстрее, так как мы делаем только 1/3 работы. imshow
также ускоряется, так как не нужно преобразовывать числа с плавающей запятой. Необъяснимым образом, чтение также стало быстрее (думаю, мы избегаем некоторых скрытых перераспределений, поскольку fr_foreground
и fr_background
всегда содержат нетронутый фрейм). Мы платим цену за дополнительное приведение в cmb()
, но в целом это кажется победой — мы на 50% от исходного времени.
Чтобы продолжить, давайте избавимся от функции cmb()
, переместим ее функциональность в main()
и разделим ее, чтобы измерить стоимость каждой из операций. Давайте также попробуем повторно использовать результат alpha.read()
(поскольку мы недавно видели улучшение производительности read()
):
times = [0.0] * 11
total_times = [0.0] * (len(times) - 1)
n = 0
while True:
times[0] = timer()
r_fg, fr_foreground = foreground.read()
r_bg, fr_background = background.read()
r_a, fr_alpha_raw = alpha.read()
if not r_fg or not r_bg or not r_a:
break # End of video
times[1] = timer()
fr_alpha = np.float32(fr_alpha_raw)
times[2] = timer()
fr_alpha *= 1/255.0
times[3] = timer()
fr_alpha_inv = 1.0 - fr_alpha
times[4] = timer()
fr_fg_weighed = fr_foreground * fr_alpha
times[5] = timer()
fr_bg_weighed = fr_background * fr_alpha_inv
times[6] = timer()
sum = fr_fg_weighed + fr_bg_weighed
times[7] = timer()
result = np.uint8(sum)
times[8] = timer()
cv2.imshow('My Image', result)
times[9] = timer()
if cv2.waitKey(1) == ord('q'): break
times[10] = timer()
update_times(times, total_times)
n += 1
Новые тайминги:
Iterations: 1786
Step 0: 6.8733 ms
Step 1: 5.2742 ms
Step 2: 1.1430 ms
Step 3: 4.5800 ms
Step 4: 7.0372 ms
Step 5: 7.0675 ms
Step 6: 5.3082 ms
Step 7: 2.6912 ms
Step 8: 0.4658 ms
Step 9: 9.6966 ms
Total: 50.1372 ms
На самом деле мы ничего не выиграли, но чтение стало заметно быстрее.
Это приводит к другой идее — что, если мы попытаемся свести к минимуму выделение памяти и повторно использовать массивы в последующих итерациях?
Мы можем предварительно выделить необходимые массивы в первой итерации (используя numpy.zeros_like
), после того как мы прочитаем первый набор фреймов:
if n == 0: # Pre-allocate
fr_alpha = np.zeros_like(fr_alpha_raw, np.float32)
fr_alpha_inv = np.zeros_like(fr_alpha_raw, np.float32)
fr_fg_weighed = np.zeros_like(fr_alpha_raw, np.float32)
fr_bg_weighed = np.zeros_like(fr_alpha_raw, np.float32)
sum = np.zeros_like(fr_alpha_raw, np.float32)
result = np.zeros_like(fr_alpha_raw, np.uint8)
Теперь мы можем использовать
Мы также можем объединить шаги 1 и 2 вместе, используя один файл numpy.multiply
.
times = [0.0] * 10
total_times = [0.0] * (len(times) - 1)
n = 0
while True:
times[0] = timer()
r_fg, fr_foreground = foreground.read()
r_bg, fr_background = background.read()
r_a, fr_alpha_raw = alpha.read()
if not r_fg or not r_bg or not r_a:
break # End of video
if n == 0: # Pre-allocate
fr_alpha = np.zeros_like(fr_alpha_raw, np.float32)
fr_alpha_inv = np.zeros_like(fr_alpha_raw, np.float32)
fr_fg_weighed = np.zeros_like(fr_alpha_raw, np.float32)
fr_bg_weighed = np.zeros_like(fr_alpha_raw, np.float32)
sum = np.zeros_like(fr_alpha_raw, np.float32)
result = np.zeros_like(fr_alpha_raw, np.uint8)
times[1] = timer()
np.multiply(fr_alpha_raw, np.float32(1/255.0), fr_alpha)
times[2] = timer()
np.subtract(1.0, fr_alpha, fr_alpha_inv)
times[3] = timer()
np.multiply(fr_foreground, fr_alpha, fr_fg_weighed)
times[4] = timer()
np.multiply(fr_background, fr_alpha_inv, fr_bg_weighed)
times[5] = timer()
np.add(fr_fg_weighed, fr_bg_weighed, sum)
times[6] = timer()
np.copyto(result, sum, 'unsafe')
times[7] = timer()
cv2.imshow('My Image', result)
times[8] = timer()
if cv2.waitKey(1) == ord('q'): break
times[9] = timer()
update_times(times, total_times)
n += 1
Это дает нам следующие тайминги:
Iterations: 1786
Step 0: 7.0515 ms
Step 1: 3.8839 ms
Step 2: 1.9080 ms
Step 3: 4.5198 ms
Step 4: 4.3871 ms
Step 5: 2.7576 ms
Step 6: 1.9273 ms
Step 7: 0.4382 ms
Step 8: 7.2340 ms
Total: 34.1074 ms
Значительное улучшение всех шагов, которые мы изменили. Мы сократили примерно 35% времени, необходимого для исходной реализации.
Незначительное обновление:
На основе ответа Silencer Я также измерил cv2.convertScaleAbs
. На самом деле он работает немного быстрее:
Step 6: 1.2318 ms
Это натолкнуло меня на другую идею: мы могли бы воспользоваться преимуществами cv2.add
< /a>, который позволяет нам указать целевой тип данных, а также выполняет преобразование насыщения. Это позволит нам объединить шаги 5 и 6 вместе.
cv2.add(fr_fg_weighed, fr_bg_weighed, result, dtype=cv2.CV_8UC3)
который выходит в
Step 5: 3.3621 ms
Опять небольшой выигрыш (раньше было около 3,9 мс).
Исходя из этого, cv2.subtract
и cv2.multiply
являются дополнительными кандидатами. Нам нужно использовать кортеж из 4 элементов для определения скаляра (сложность привязки Python), и нам нужно явно определить тип выходных данных для умножения.
cv2.subtract((1.0, 1.0, 1.0, 0.0), fr_alpha, fr_alpha_inv)
cv2.multiply(fr_foreground, fr_alpha, fr_fg_weighed, dtype=cv2.CV_32FC3)
cv2.multiply(fr_background, fr_alpha_inv, fr_bg_weighed, dtype=cv2.CV_32FC3)
Тайминги:
Step 2: 2.1897 ms
Step 3: 2.8981 ms
Step 4: 2.9066 ms
Кажется, это все, что мы можем сделать без некоторого распараллеливания. Мы уже пользуемся тем, что может предоставить OpenCV с точки зрения отдельных операций, поэтому нам следует сосредоточиться на конвейерной обработке нашей реализации.
Чтобы помочь мне понять, как разделить код между различными этапами конвейера (потоками), я сделал диаграмму, которая показывает все операции, наше лучшее время для них, а также взаимозависимости для вычислений:
![введите здесь описание изображения](https://i.stack.imgur.com/6kQjO.png)
WIP см. комментарии для получения дополнительной информации, пока я пишу это.
person
Dan Mašek
schedule
09.05.2018
/255
не будет работать так же, как в 2.x... - person Dan Mašek   schedule 09.05.2018