Потокобезопасность и медленный pyplot.hist

Функция hist в matplotlib.pyplot работает очень медленно, что, по-видимому, связано с выбранной мной структурой. Я создал в Tkinter переднюю панель, которая запускает цикл управления камерой. Чтобы обеспечить оперативность цикла управления, я создал класс ImageProcessor, который собирает, обрабатывает и отображает изображения в cv2. Объект ImageProcessor выполняется в своем собственном потоке. Это работает до тех пор, пока я не пытаюсь построить гистограмму изображения.

Поскольку Tkinter не является потокобезопасным, я использую Agg в качестве бэкэнда и рисую нарисованный холст pyplot.figure с помощью cv2. Расчет гистограммы изображения с помощью pyplot.hist занимает более 20 секунд. Самостоятельный расчет гистограммы занимает всего 0,5 секунды.

Как это проявляется? Нужно ли запускать Matplotlib из основного потока или этого достаточно, если с ним взаимодействует только один поток (как в моем случае)? Или в моем коде есть еще одно недоразумение?

import threading
import time
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from timeit import default_timer as timer
from datetime import timedelta
import queue


class ImageProcessor(threading.Thread):
    def __init__(self):
        matplotlib.use('Agg')
        threading.Thread.__init__(self)

        # initialize plot for histograms
        self.hist_fig = plt.figure()

        self.loop = True
        self.continuous_acquisition_var = False
        self.a = None

    def run(self):

        while self.loop:

            self.a = np.random.uniform(low=0, high=16384, size=12320768).reshape((4096, 3008))

            self.hist_fig.clf()  # clear histogram plot

            start = timer()
            plt.hist(self.a.flatten(), bins=256, range=(0.0, 16384), fc='r', ec='r')
            end = timer()
            print(timedelta(seconds=end - start))

    def stop(self):
        self.loop = False


def ctl_loop(command):
    ctl_loop_var = True

    img_proc = ImageProcessor()
    img_proc.daemon = True
    img_proc.start()

    while ctl_loop_var:  # main loop

        while not command.empty():
            q_element = command.get()
            task = q_element[0]
            data = q_element[1]

            func = getattr(img_proc, task)
            func(data)

            if task == "stop":
                ctl_loop_var = False


if __name__ == '__main__':
    cmd_queue = queue.Queue()

    ctl = threading.Thread(target=ctl_loop, args=(cmd_queue, ))
    ctl.daemon = True
    ctl.start()

    time.sleep(40)

    cmd_queue.put(('stop', ''))

Я мог отследить его до этого звонка от np.histogram. Так что, похоже, это не связано с matplotlib.

Michael Butscher 19.06.2024 12:51

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

JohanC 19.06.2024 13:21

@MichaelButscher спасибо за этот анализ. Что бы вы тогда предложили? Я пытался улучшить ситуацию при обновлении до numpy 1.26.4, но это не помогло.

JMP 19.06.2024 13:35

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

JMP 19.06.2024 13:38

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

Michael Butscher 19.06.2024 13:55
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
3
5
107
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Решение простое и не имеет ничего общего с plt.hist. Просто добавьте строку time.sleep(0.01) в основной цикл. Причина в том, что многопоточность — это не то же самое, что многопроцессорность. Все потоки используют один и тот же процесс (ЦП), то есть одновременно может выполняться только один поток. В вашем случае основной поток (цикл while ctl_loop_var) как можно быстрее проверяет, истинно ли ctl_loop_var, не позволяя другому потоку что-либо делать. Поэтому убедитесь, что вы не создаете ненужную нагрузку на процессор. Это относится и к многопроцессорной обработке, хотя влияние может быть менее заметным.

def ctl_loop(command):
    ctl_loop_var = True

    img_proc = ImageProcessor()
    img_proc.daemon = True
    img_proc.start()

    while ctl_loop_var:  # main loop

        while not command.empty():
            q_element = command.get()
            task = q_element[0]
            data = q_element[1]

            if task == "stop":
                img_proc.stop()
                ctl_loop_var = False

            else:
                func = getattr(img_proc, task)
                func(data)
    
        time.sleep(.01)  # give the other thread time to process

Кроме того, код также исправляет две ошибки в исходном коде:

  1. ImageProcessor.stop принимает только один аргумент
  2. ImageProcessor.stop не останавливал поток должным образом, когда основной поток перегружен.

Я также заметил, что ваша реализация с plt.hist(..., bins=256, range=(0.0, 16384)) примерно в 7 раз быстрее, чем с plt.hist(..., bins=list_of_bins)! Не меняйте его ;).

спасибо за ваш анализ. Перегрузка процессора непроверенным циклом while — довольно глупая ошибка. Я должен был увидеть это сам... Спасибо за вашу поддержку.

JMP 25.06.2024 11:10

Если вы хотите довести скорость до предела. Попробуйте рассчитать гистограмму с помощью numba: numba.pydata.org/numba-examples/examples/density_estimation/‌​…

kho 26.06.2024 12:21

Другие вопросы по теме