Как оптимизировать многопроцессорность в Python

EDIT: У меня возникли вопросы о том, что такое поток видео, поэтому я буду предлагать больше ясности. Поток представляет собой живой видеоролик из моей веб-камеры, доступ к которому осуществляется через OpenCV. Я получаю каждый кадр, когда камера читает его, и отправляю его в отдельный процесс для обработки. Процесс возвращает текст, основанный на вычислениях, выполненных на изображении. Затем текст отображается на изображении. Мне нужно отобразить поток в реальном времени, и это нормально, если есть разрыв между текстом и отображаемым видео (т.е. Если текст применим к предыдущему кадру, это нормально).

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

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

Я использую модуль многопроцессорности в python, чтобы ускорить мою основную программу. Однако я считаю, что я мог бы делать что-то неправильно, поскольку я не думаю, что вычисления происходят довольно параллельно.

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

Однако основной процесс, по-видимому, отстает, когда я использую многопроцессорную обработку, работаю примерно на половину быстрее, чем без нее, что заставляет меня думать, что процессы не работают полностью параллельно.

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

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

Таким образом, я считаю, что либо

1) Я не оптимально использую многопроцессорность

ИЛИ

2) Эти процессы не могут быть реализованы параллельно (я бы понял небольшое отставание, но это замедляло основной процесс пополам).

Вот схема моего кода. Существует только один потребитель вместо 2, но оба потребителя почти идентичны. Если бы кто-нибудь мог предложить руководство, я был бы признателен.

class Consumer(multiprocessing.Process):

    def __init__(self, task_queue, result_queue):
        multiprocessing.Process.__init__(self)
        self.task_queue = task_queue
        self.result_queue = result_queue
        #other initialization stuff

    def run(self):
        while True:
            image = self.task_queue.get()
            #Do computations on image
            self.result_queue.put("text")

        return

import cv2

tasks = multiprocessing.Queue()
results = multiprocessing.Queue()
consumer = Consumer(tasks,results)
consumer.start()

#Creating window and starting video capturer from camera
cv2.namedWindow("preview")
vc = cv2.VideoCapture(0)
#Try to get the first frame
if vc.isOpened():
    rval, frame = vc.read()
else:
    rval = False

while rval:
    if tasks.empty():
       tasks.put(image)
    else:
       text = tasks.get()
       #Add text to frame
       cv2.putText(frame,text)

    #Showing the frame with all the applied modifications
    cv2.imshow("preview", frame)

    #Getting next frame from camera
    rval, frame = vc.read()

Ответ 1

Я хочу, чтобы моя программа читала изображения из видеопотока в основном процессе

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

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

Также было бы неплохо ограничить размер очереди задач, чтобы избежать ее сбегания с использованием памяти, если обработка не может идти в ногу с входным потоком. Можно указать размер при вызове Queue(<size>). Если очередь имеет такой размер, вызовы .put будут блокироваться до тех пор, пока очередь не будет заполнена.

import time
import multiprocessing
import cv2

class ImageProcessor(multiprocessing.Process):

    def __init__(self, tasks_q, results_q):
        multiprocessing.Process.__init__(self)
        self.tasks_q = tasks_q
        self.results_q = results_q

    def run(self):
        while True:
            image = self.tasks_q.get()
            # Do computations on image
            time.sleep(1)
            # Display the result on stream
            self.results_q.put("text")

# Tasks queue with size 1 - only want one image queued
# for processing. 
# Queue size should therefore match number of processes
tasks_q, results_q = multiprocessing.Queue(1), multiprocessing.Queue()
processor = ImageProcessor(tasks_q, results_q)
processor.start()

def capture_display_video(vc):
    rval, frame = vc.read()
    while rval:    
        image = frame.get_image()
        if not tasks_q.full():
            tasks_q.put(image)
        if not results_q.empty():
            text = results_q.get()
            cv2.putText(frame, text)
        cv2.imshow("preview", frame)
        rval, frame = vc.read()

cv2.namedWindow("preview")
vc = cv2.VideoCapture(0)
if not vc.isOpened():
    raise Exception("Cannot capture video")

capture_display_video(vc)
processor.terminate()

Ответ 2

(Обновленное решение на основе последнего кода)

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

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

Обратите внимание, что я принудительно вызываю вызовы get/put с помощью try/catch. На документ, пустой и полный, не на 100% точны.

import cv2
import multiprocessing
import random
from time import sleep

class Consumer(multiprocessing.Process):

    def __init__(self, task_queue, result_queue):
        multiprocessing.Process.__init__(self)
        self.task_queue = task_queue
        self.result_queue = result_queue
        # Other initialization stuff

    def run(self):
        while True:
            frameNum, frameData = self.task_queue.get()
            # Do computations on image
            # Simulate a processing longer than image fetching
            m = random.randint(0, 1000000)
            while m >= 0:
                m -= 1
            # Put result in queue
            self.result_queue.put("result from image " + str(frameNum))

        return

# No more than one pending task
tasks = multiprocessing.Queue(1)
results = multiprocessing.Queue()
# Init and start consumer
consumer = Consumer(tasks,results)
consumer.start()

#Creating window and starting video capturer from camera
cv2.namedWindow("preview")
vc = cv2.VideoCapture(0)
#Try to get the first frame
if vc.isOpened():
    rval, frame = vc.read()
    frame = cv2.resize(frame, (0,0), fx=0.5, fy=0.5)
else:
    rval = False

# Dummy int to represent frame number for display
frameNum = 0
# String for result
text = None

font = cv2.FONT_HERSHEY_SIMPLEX

# Process loop
while rval:
    # Grab image from stream
    frameNum += 1
    # Put image in task queue if empty
    try:
        tasks.put_nowait((frameNum, frame))
    except:
        pass
    # Get result if ready
    try:
        # Use this if processing is fast enough
        # text = results.get(timeout=0.4)
        # Use this to prefer smooth display over frame/text shift
        text = results.get_nowait()
    except:
        pass

    # Add last available text to last image and display
    print("display:", frameNum, "|", text)
    # Showing the frame with all the applied modifications
    cv2.putText(frame,text,(10,25), font, 1,(255,0,0),2)
    cv2.imshow("preview", frame)
    # Getting next frame from camera
    rval, frame = vc.read()
    # Optional image resize
    # frame = cv2.resize(frame, (0,0), fx=0.5, fy=0.5)

Вот несколько выводов, вы можете увидеть задержку между изображением и результатом и результат отвлечения.

> ('display:', 493, '|', 'result from image 483')
> ('display:', 494, '|', 'result from image 483')
> ('display:', 495, '|', 'result from image 489')
> ('display:', 496, '|', 'result from image 490')
> ('display:', 497, '|', 'result from image 495')
> ('display:', 498, '|', 'result from image 496')

Ответ 3

Здесь более элегантное решение (IMHO), которое использует несколько процессов для обработки ваших кадров:

def process_image(args):
    image, frame = args
    #Do computations on image
    return "text", frame

import cv2

pool = multiprocessing.Pool()

def image_source():
    #Creating window and starting video capturer from camera
    cv2.namedWindow("preview")
    vc = cv2.VideoCapture(0)
    #Try to get the first frame
    if vc.isOpened():
        rval, frame = vc.read()
    else:
        rval = False

    while rval:
        yield image, frame
        # Getting next frame from camera
        rval, frame = vc.read()

for (text, frame) in pool.imap(process_image, image_source()):
    # Add text to frame
    cv2.putText(frame, text)
    # Showing the frame with all the applied modifications
    cv2.imshow("preview", frame)

Pool.imap должен позволить вам выполнять итерацию результатов пула, пока он все еще обрабатывает другие изображения с вашей камеры.

Ответ 4

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

import time
from multiprocessing import Pool

def process_frame(frame_id, frame_data):
    # this function simulates the processing of the frame.
    # I used a longer sleep thinking that it takes longer
    # and therefore the reason of parallel processing.
    print("..... got frame {}".format(frame_id))
    time.sleep(.5)
    char = frame_data[frame_id]
    count = frame_data.count(char)
    return frame_id, char, count

def process_result(res):
    # this function simulates the function that would receive
    # the result from analyzing the frame, and do what is
    # appropriate, like printing, making a dict, saving to file, etc.
    # this function is called back when the result is ready.
    frame_id, char, count = res
    print("in frame {}".format(frame_id), \
           ", character '{}' appears {} times.".format(
                        chr(char), count))



if __name__ == '__main__':

    pool = Pool(4)
    # in my laptop I got these times:
    # workers, time
    #   1     10.14
    #   2      5.22
    #   4      2.91
    #   8      2.61 # no further improvement after 4 workers.
                    # your case may be different though.

    from datetime import datetime as dt
    t0 = dt.now()

    for i in range(20):   # I limited this loop to simulate 20 frames
                          # but it could be a continuous stream,
                          # that when finishes should execute the
                          # close() and join() methods to finish
                          # gathering all the results.


        # The following lines simulate the video streaming and
        # your selecting the frames that you need to analyze and
        # send to the function process_frame.
        time.sleep(0.1)
        frame_id = i
        frame_data = b'a bunch of binary data representing your frame'

        pool.apply_async(  process_frame,                #func
                           (frame_id, frame_data),       #args
                           callback=process_result       #return value
                        )

    pool.close()
    pool.join()

    print(dt.now() - t0)

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

Ответ 5

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

Следующий код печатает письмо из "lorem ipsum", выбирая одну из 6 букв (кадров). Поскольку существует отставание, нам нужен буфер, который я выполнил с помощью deque. После того, как буфер продвинулся, отображение кадра и заголовка синхронизируется.

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

import time
import random
random.seed(1250)
from multiprocessing import Pool, Manager
from collections import deque


def display_stream(stream, pool, queue, buff, buffered=False):
    delay = 24
    popped_frames = 0
    for i, frame in enumerate(stream):
        buff.append([chr(frame), ''])
        time.sleep(1/24 * random.random()) # suppose a 24 fps video
        if i % 6 == 0:                     # suppose one out of 6 frames
            pool.apply_async(process_frame, (i, frame, queue))
        ii, caption = (None, '') if queue.empty() else queue.get()
        if buffered:
            if ii is not None:
                buff[ii - popped_frames][1] = caption
            if i > delay:
                print(buff.popleft())
                popped_frames += 1
        else:
            lag = '' if ii is None else i - ii
            print(chr(frame), caption, lag)

    else:
        pool.close()
        pool.join()
        if buffered:
            try:
                while True:
                    print(buff.popleft())
            except IndexError:
                pass


def process_frame(i, frame, queue):
    time.sleep(0.4 * random.random())      # suppose ~0.2s to process
    caption = chr(frame).upper()           # mocking the result...
    queue.put((i, caption))


if __name__ == '__main__':

    p = Pool()
    q = Manager().Queue()
    d = deque()

    stream = b'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'

    display_stream(stream, p, q, b)

Ответ 6

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

def setaffinity(mask = 128): # 128 is core 7
    pid    = win32api.GetCurrentProcessId()
    handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, True, pid)
    win32process.SetProcessAffinityMask(handle, mask)
    return