Многопоточность OpenCV и Python - поиск в объекте VideoCapture - PullRequest
0 голосов
/ 05 октября 2018

Я работал над приложением python, которое использует OpenCV для чтения кадров из видео и создания композита «активности», то есть того, что изменилось с одного кадра на другой.Чтобы сделать это, я действительно хочу проверять только один кадр в секунду или около того.

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

video_capture = cv2.VideoCapture(video_fullpath)
this_frame = get_frame(0)

def get_frame(time):
    video_capture.set(cv2.CAP_PROP_POS_MSEC, time)
    capture_success, this_frame = video_capture.read()
    return this_frame

Процесс получения последующих кадров, используя последние две строки кода выше, действительно медленный.На MacBook Pro 2015 года для получения каждого кадра требуется 0,3–0,4 с (с интервалом в 1 с в видео, которое составляет ~ 100 МБ .mp4 видеофайл).Для сравнения, остальные мои операции, которые сравнивают каждый кадр с его предшественником, выполняются очень быстро - обычно менее 0,01 с.

Поэтому я смотрел на многопоточность, но я изо всех сил.

Я могу получить многопоточность, работающую на основе «упреждения», то есть, пока я обрабатываю один кадр, я могу получить следующий.И как только я закончу обработку предыдущего кадра, я буду ждать завершения операции «lookahead», прежде чем продолжить.Я делаю это с помощью следующего кода:

while True:
    this_frame, next_frame_thread = get_frame_async(prev_frame.time + time_increment)
    << do processing of this_frame ... >>
    next_frame_thread.join()

def get_frame_async(time):
    if time not in frames:
        frames[time] = get_frame(time)
    next_frame_thread = Thread(target=get_frame, args=(time,))
    next_frame_thread.start()
    return frames[time], next_frame_thread

Кажется, что вышеприведенное работает, но поскольку операция поиска очень медленная по сравнению со всем остальным, она на самом деле не экономит много времени - на самом деле это трудноувидеть какую-либо выгоду вообще.

Затем я подумал, могу ли я получать несколько кадров параллельно.Однако всякий раз, когда я пытаюсь это сделать, я получаю ряд ошибок, в основном связанных с async_lock (например, Assertion fctx->async_lock failed at libavcodec/pthread_frame.c:155).Интересно, это просто, что объект OpenCV VideoCapture не может искать в нескольких местах одновременно ... что кажется разумным.Но если это правда, есть ли способ значительно ускорить эту операцию?

Я использовал несколько разных источников, в том числе этот https://nrsyed.com/2018/07/05/multithreading-with-opencv-python-to-improve-video-processing-performance/, который показывает огромные ускорения, но яЯ борюсь с тем, почему я получаю эти ошибки вокруг async_lock.Это просто операция поиска?Я не могу найти никаких примеров многопоточности во время поиска по видео - просто пример людей, читающих все кадры последовательно.

Любые советы или рекомендации о том, где / какие части наиболее вероятно выиграют от многопоточности (или другой подход).) будет приветствоваться.Это моя первая попытка многопоточности, поэтому полностью признаю, что я мог упустить что-то очевидное!Основываясь на этой странице (https://www.toptal.com/python/beginners-guide-to-concurrency-and-parallelism-in-python), Я был немного ошеломлен разнообразием доступных опций.

Спасибо!

Ответы [ 2 ]

0 голосов
/ 21 октября 2018

По совпадению, я работал над аналогичной проблемой, и я создал библиотеку Python (более тонкая оболочка) для чтения видео.Библиотека называется mydia .

Библиотека не не использует OpenCV.Он использует FFmpeg в качестве бэкэнда для чтения и обработки видео.

mydia поддерживает выбор пользовательского кадра, изменение размера кадра, преобразование в градациях серого и многое другое.С документацией можно ознакомиться здесь

Итак, если вы хотите выбрать N кадров в секунду (где N = 1 в вашем случае), следующий код сделает это:

import numpy as np
from mydia import Videos

video_path = "path/to/video"

def select_frames(total_frames, num_frames, fps, *args):
    """This function will return the indices of the frames to be captured"""
    N = 1
    t = np.arange(total_frames)
    f = np.arange(num_frames)
    mask = np.resize(f, total_frames)

    return t[mask < N][:num_frames].tolist()

# Let's assume that the duration of your video is 120 seconds
# and you want 1 frame for each second 
# (therefore, setting `num_frames` to 120)
reader = Videos(num_frames=120, mode=select_frames)

video = reader.read(video_path)  # A video tensor/array

Самое приятное то, что внутренне, только те кадры, которые требуются, читаются, и, следовательно, процесс намного быстрее (что, я полагаю, вы ищете).

Установка mydia чрезвычайно проста и ее можно посмотреть здесь .

Это может иметь небольшую кривую обучения, но я считаю, что это именно то, что вы ищете.

Более того, если у вас есть несколько видео, вы можете использовать несколько рабочих для их параллельного чтения.Например:

from mydia import Videos

path = "path/to/video"
reader = Videos()
video = reader.read(path, workers=4)

В зависимости от вашего процессора, это может дать вам значительное ускорение.

Надеюсь, это поможет !!

0 голосов
/ 06 октября 2018

Основываясь на комментариях к исходному вопросу, я провел некоторое тестирование и подумал, что стоит поделиться (интересными) результатами.Большой потенциал экономии для тех, кто использует OpenCV VideoCapture.set(CAP_PROP_POS_MSEC) или VideoCapture.set(CAP_PROP_POS_FRAMES).

. Я провел профилирование, сравнив три варианта:

1.ПОЛУЧИТЕ КАДРЫ, ПОИСКА ВРЕМЕНИ:

frames = {}
def get_all_frames_by_ms(time):
    while True:
        video_capture.set(cv2.CAP_PROP_POS_MSEC, time)
        capture_success, frames[time] = video_capture.read()
        if not capture_success:
            break
        time += 1000

2.ПОЛУЧИТЕ КАДРЫ, ПОИСКА НОМЕР КАДРА:

frames = {}
def get_all_frames_by_frame(time):
    while True:
        # Note my test video is 12.333 FPS, and time is in milliseconds
        video_capture.set(cv2.CAP_PROP_POS_FRAMES, int(time/1000*12.333))
        capture_success, frames[time] = video_capture.read()
        if not capture_success:
            break
        time += 1000

3.ПОЛУЧИТЕ РАМКИ, ПОЛУЧИВАЯ ВСЕ, НО ПОЛУЧИТЬ ТОЛЬКО ОДНОГО, КОТОРОГО Я ХОЧУ:

def get_all_frames_in_order():
    prev_time = -1
    while True:
        grabbed = video_capture.grab()
        if grabbed:
            time_s = video_capture.get(cv2.CAP_PROP_POS_MSEC) / 1000
            if int(time_s) > int(prev_time):
                # Only retrieve and save the first frame in each new second
                self.frames[int(time_s)] = video_capture.retrieve()
            prev_time = time_s
        else:
            break

При выполнении этих трех подходов время (из трех прогонов каждого) следующее:

  1. 33,78 с 29,65 с 29,24 с
  2. 31,95 с 29,16 с 28,35 с
  3. 11,81 с 10,76 с 11,73 с

В каждом случае сохраняется 100 кадров приИнтервалы 1 с в словарь, где каждый кадр представляет собой изображение 3072x1728 из видеофайла .mp4.Все на MacBookPro 2015 года с 2,9 ГГц процессором Intel Core i5 и 8 ГБ ОЗУ.

Выводы ... если вы заинтересованы в извлечении только нескольких кадров из видео, тогда стоит посмотреть на просмотр всех кадров.по порядку и захватывая их все, но извлекая только те, которые вас интересуют - как альтернативу чтению (которое захватывает и извлекает за один раз).У меня было почти 3-кратное ускорение.

Я также пересмотрел многопоточность на этой основе.У меня есть два процесса тестирования - один, который получает кадры, и другой, который обрабатывает их, как только они становятся доступными:

frames = {}

def get_all_frames_in_order():
    prev_time = -1
    while True:
        grabbed = video_capture.grab()
        if grabbed:
            time_s = video_capture.get(cv2.CAP_PROP_POS_MSEC) / 1000
            if int(time_s) > int(prev_time):
                # Only retrieve and save the first frame in each new second
                frames[int(time_s)] = video_capture.retrieve()
            prev_time = time_s
        else:
            break

def process_all_frames_as_available(processing_time):
    prev_time = 0
    while True:
        this_time = prev_time + 1000
        if this_time in frames and prev_time in frames:
            # Dummy processing loop - just sleeps for specified time
            sleep(processing_time)
            prev_time += self.time_increment
            if prev_time + self.time_increment > video_duration:
                break
        else:
            # If the frames aren't ready yet, wait a short time before trying again
            sleep(0.02)

Для этого тестирования я затем вызывал их один за другим (последовательно,однопоточное) или со следующим многопоточным кодом:

get_frames_thread = Thread(target=get_all_frames_in_order)
get_frames_thread.start()
process_frames_thread = Thread(target=process_all_frames_as_available, args=(0.02,))
process_frames_thread.start()
get_frames_thread.join()
process_frames_thread.join()

Исходя из этого, я теперь счастлив, что многопоточность работает эффективно и экономит значительное количество времени.Я сгенерировал временные интервалы для двух указанных выше функций по отдельности, а затем вместе в однопоточном и многопоточном режимах.Результаты приведены ниже (число в скобках - это время в секундах, которое занимает «обработка» для каждого кадра, что в данном случае является просто фиктивной / задержкой):

get_all_frames_in_order - 2.99s

Process time = 0.02s per frame:
process_all_frames_as_available - 0.97s
single-threaded - 3.99s
multi-threaded - 3.28s

Process time = 0.1s per frame:
process_all_frames_as_available - 4.31s
single-threaded - 7.35s
multi-threaded - 4.46s

Process time = 0.2s per frame:
process_all_frames_as_available - 8.52s
single-threaded - 11.58s
multi-threaded - 8.62s

Как вы можете надеяться, вы увидите,результаты многопоточности очень хороши.По сути, для одновременного выполнения обеих функций на ~ 0,2 с требуется больше времени, чем для медленной работы двух функций, выполняемых совершенно по-отдельности.

Надеюсь, это кому-нибудь поможет!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...