Если бы вы когда-либо писали свой собственный код для обработки видео с помощью OpenCV, MoviePy или любой из множества других библиотек, вы, вероятно, столкнулись бы с проблемой ужасно медленной обработки.

Это потому, что код, который мы пишем, в основном написан для работы только на одном ядре. У нас есть задача, которая огромна по объему данных, с которыми она должна работать, и нет решения, которое просто упрощает нашу жизнь из коробки. Однако нам бы хотелось совсем другого сюжета. Это лучше всего резюмируется некоторыми текстами Daft Punk.

Работай, сделай это ... Сложнее, лучше, БЫСТРЕЕ, сильнее

Увидев слово быстрее, первая мысль, которая пришла вам в голову, была, вероятно, параллелизмом. И когда мы думаем о распараллеливании обработки видео, это то, как это должно быть сделано, верно?

Подход # 1

Мы могли бы просто назначать фреймы один за другим любому свободному ядру. Кажется, простое и очевидное решение, не так ли?

К сожалению, это не совсем так. Фактически, во многих случаях он работает хуже, чем наш простой одноядерный код. Чтобы понять, почему, мы должны углубиться в то, как хранятся сами видео. Поскольку кодирование видео является последовательным, в то время как одно ядро ​​декодирует кадр, другие ядра должны бездействовать. Они не могут начать обработку следующих кадров, пока это ядро ​​не декодирует предыдущий (Вот (буквально) технический рассказ о том, как работает сжатие видео).

Такие библиотеки, как joblib, также пытаются распараллеливать задания аналогичным образом и поэтому не дают того ускорения, которое мы хотели бы видеть. Это означает, что мы должны искать лучшую альтернативу

Подход # 2

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

Хотя это блестящая идея сама по себе, большим ограничением этого подхода является то, что он может одновременно использовать только два потока / ядра вашей машины. Кроме того, по какой-то причине код в блоге Адриана, похоже, не распространяется напрямую на Python 3 (см. Комментарии к сообщению. Возможно, Python изменил внутреннюю работу своей multithreading библиотеки?) В любом случае, если мы приложим некоторые усилия в этом направлении мы должны быть в состоянии прийти к тому моменту, когда нам, по крайней мере, немного лучше, чем в том месте, откуда мы начали.

Тем не менее, это не совсем то ускорение, которого мы ожидали. Мы бы хотели оставить Threadripper или Xeon для массового распределения рабочей нагрузки в одном видео. И чтобы иметь возможность сделать что-то подобное, давайте вернемся к Подходу №1. Основной проблемой нашего первого подхода была взаимозависимость между ядрами. Это неизбежно из-за того, что кодирование видео принципиально «блокирует».

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

Подход # 3

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

Скажем, у нас есть 10 000 кадров и 4 ядра, это будет означать, что мы выделяем каждое ядро ​​для работы с фиксированной последовательной четвертью (= 2500) из этих 10 000 кадров. Таким образом, ядрам не нужно ждать друг друга для обработки следующих кадров. Единственная задача, которая остается после этого, - это заново собрать обработанные кадры в правильном порядке. Следование этой методологии позволит нам легко распараллелить конвейер обработки видео.

Сделать это в Python настолько просто, насколько это возможно. Для справки, вероятно, так будет выглядеть ваш обычный код обработки видео:

Если бы нам пришлось распараллеливать этот код, нам пришлось бы использовать библиотеку python multiprocessing

И большая часть сделана. Однако важная часть все еще упущена. У нас остались только обработанные фрагменты. Нам еще нужно объединить эти фрагменты. Это можно легко сделать с помощью ffmpeg следующим образом:

И вот оно! Метод распараллеливания конвейеров обработки видео, масштабируемый в зависимости от количества ядер, которые мы ему добавляем.

Не верьте мне на слово: D. Попробуйте сами и дайте мне знать в комментариях ниже. Весь код доступен в GitHub по адресу https://github.com/rsnk96/fast-cv.

Примечания:

  • Если вам интересно, как cv2.set(CAP_PROP_POS_FRAMES) переходит к определенному кадру без необходимости декодировать его с помощью предыдущих кадров, то это потому, что он переходит к ближайшему ключевому кадру. Посмотрите видео по ссылке, чтобы лучше понять это
  • cv2.set(CAP_PROP_POS_FRAMES) известно, что неточно ищет указанный кадр. Возможно, это связано с тем, что он ищет ближайший ключевой кадр. Это означает, что может быть повторение пары кадров в тех точках, где видео фрагментировано. Поэтому не рекомендуется применять этот метод для критически важных случаев использования.
  • На самом деле наилучшая производительность может быть получена при сочетании подходов 2 и 3. Но это потребует немного больше усилий для его кодирования. Дайте мне знать в комментариях, если кому-нибудь удастся собрать код для этого! :)
  • **: включение проверки на if ret==False: break является обычной практикой, но я избегал этого здесь для простоты.
  • Этот пост представляет собой краткое изложение выступления, которое я недавно провел в Писангамаме и Пиконе, Индия. Https://www.youtube.com/watch?v=v29nBvfikcE