Фото Кристиана Видигера на Unsplash
В этом уроке мы рассмотрим, как мы можем ускорить научные вычисления, используя Многоподобная
в реальном примере. В частности, мы обнаружим местоположение всех ядер в изображениях флуоресцентной микроскопии от общественности Набор данных по живописи MCF7 выпущен Брод Институтом. После завершения этого учебника вы узнаете, как сделать следующее:
- Обнаружение ядер на изображениях флуоресцентной микроскопии
- Ускорить обработку изображений с помощью
многопроцессорная. Бассейн
- Уменьшить накладные расходы сериализации с помощью копии на записи (только Unix)
- Добавьте панель прогресса в параллельные задачи обработки
Загрузите набор данных
Примечание. Данные визуализации составляют почти 1 ГБ только для одной пластины. Вам понадобится несколько ГБ пространства, чтобы завершить этот урок.
Во -первых, загрузите данные изображения для первой таблички набора данных по живописи ячейки здесь в папку под названием изображения/
и извлечь это. Вы должны получить папку под названием Изображения/неделя1_22123/
наполнены изображениями TIFF. Изображения названы в соответствии со следующей конвенцией:
# Template {week}_{plate}_{well}_{field}_{channel}{id}.tif # Example Week1_150607_B02_s4_w1EB868A72-19FC-46BB-BDFA-66B8966A817C.tif
Найдите все изображения DAPI
DAPI – это флуоресцентный краситель, который связывается с ДНК и окрашивает ядра в каждом изображении. Как правило, сигнал DAPI будет захвачен в первом канале микроскопа. Сначала нам нужно проанализировать имена изображений, чтобы найти изображения с первого канала ( W1
), чтобы мы могли обнаружить ядра только из изображений DAPI.
from glob import glob paths = glob('images/Week1_22123/Week1_*_w1*.tif') paths.sort() print(len(paths)) # => 240
Здесь мы использовали глобус
и шаблон с подстановочными знаками, чтобы найти пути ко всем изображениям DAPI. Видимо, есть 240 изображений DAPI, которые нам нужно обрабатывать! Мы можем загрузить первое изображение, чтобы увидеть, с чем мы работаем.
from skimage import io import matplotlib.pyplot as plt img = io.imread(paths[0]) print(img.shape, img.dtype, img.min(), img.max()) # => (1024, 1280) uint16 176 10016 plt.figure(figsize=(6, 6)) plt.imshow(img, cmap='gray', vmax=4000) plt.axis('off') plt.show()
Мы видим, что изображения являются (1024, 1280) массивами 16-битных целых чисел без знака. Это конкретное изображение имеет интенсивности пикселей в диапазоне от 176 до 10 016.
Использование обезживания для обнаружения ядер
Scikit-Image-это пакет Python для обработки изображений, которую мы можем использовать для обнаружения ядер в этих изображениях DAPI. Простой и эффективный подход состоит в том, чтобы сгладить изображение, чтобы уменьшить шум, а затем найти все локальные максимумы, которые превышают заданный порог интенсивности. Давайте напишем функцию, чтобы сделать это на нашем примере изображения.
from skimage.filters import gaussian from skimage.feature import peak_local_max def detect_nuclei(img, sigma=4, min_distance=6, threshold_abs=1000): g = gaussian(img, sigma, preserve_range=True) return peak_local_max(g, min_distance, threshold_abs) centers = detect_nuclei(img) print(centers.shape) # => (214, 2) plt.figure(figsize=(6, 6)) plt.imshow(img, cmap='gray', vmax=4000) plt.plot(centers[:, 1], centers[:, 0], 'r.') plt.axis('off') plt.show()
Теперь мы где -то получаем! Detect_nuclei
Функция принимает img
массив и возвращает массив ядер (x, y) -Кординаты, называемые Центры
Анкет Сначала он сглаживает IMG
массив, применив Гауссовый
размытие с сигма
. reverve_range
Аргумент предотвращает Skimage
от нормализации изображения, чтобы мы вернули сглаженное изображение g
в том же диапазоне интенсивности, что и вход. Затем мы используем PEAK_LOCAL_MAX
Функция для обнаружения всех локальных максимумов в нашем сглаженном изображении. Мы устанавливаем min_distance
чтобы не допустить, чтобы какие -либо два ядра не были нереалистично близко друг к другу и threshold_abs
Чтобы избежать обнаружения ложных срабатываний в темных областях изображения.
Примечание: мы будем использовать это Detect_nuclei
функция во всех последующих экспериментах.
Метод 1 – Понимание списка с io
Теперь, когда у нас есть функция для обнаружения координат ядер, давайте применим эту функцию ко всем нашим изображениям DAPI, используя простое понимание списка.
from tqdm.notebook import tqdm def process_images1(paths): return [detect_nuclei(io.imread(p)) for p in paths] meth1_times = %timeit -n 4 -r 1 -o centers = process_images1(tqdm(paths)) # => 18 s ± 0 ns per loop (mean ± std. dev. of 1 run, 4 loops each)
В Process_images1
, мы заключаем список путей изображения и используем понимание списка для загрузки каждого изображения и обнаружения ядер. Используя %timeit
Волшебная команда в Юпитере, мы видим, что этот подход имеет среднее время исполнения ~ 18 секунд.
Метод 2 – Многопроцессия. Бассейн с io
Посмотрим, сможем ли мы ускорить ситуацию с обработкой Бассейн
Анкет Для этого нам нужно определить функцию для завершения Detect_nuclei
и io.imread
вместе, чтобы мы могли карта
Это функционирует над списком путей изображения.
import multiprocessing as mp def _process_image(path): return detect_nuclei(io.imread(path)) def process_images2(paths): with mp.Pool() as pool: return pool.map(_process_image, paths) meth2_times = %timeit -n 4 -r 1 -o centers = process_images2(paths) # => 5.54 s ± 0 ns per loop (mean ± std. dev. of 1 run, 4 loops each)
Намного лучше. Это было запущено на машине с CPU_COUNT ()
Анкет Теперь та же задача имеет среднее время выполнения ~ 5,5 секунды (> 3 раза ускорение).
Метод 3 – Понимание списка из памяти
Что произойдет, если мы прочитаем все изображения в память перед обнаружением ядер? Предположительно, этот подход будет быстрее, потому что нам не пришлось бы читать данные изображения с диска во время %timeit
командование Давай попробуем.
import numpy as np images = np.asarray([io.imread(p) for p in tqdm(paths)]) def process_images3(images): return [detect_nuclei(img) for img in images] meth3_times = %timeit -n 4 -r 1 -o centers = process_images3(tqdm(images)) # => 17.7 s ± 0 ns per loop (mean ± std. dev. of 1 run, 4 loops each)
Теперь понимание списка немного быстрее, но только примерно на 0,3 секунды. Это говорит о том, что чтение данных изображения не является ограничивающим скоростью шаг в методе 1, а скорее Detect_nuclei
вычисления.
Метод 4 – Многопроцессия. Пул от памяти
Просто для полноты, давайте попробуем использовать многопроцессорная. Бассейн
к карта
Наш Detect_nuclei
функционируйте по всем изображениям в памяти.
def process_images4(images): with mp.Pool() as pool: return pool.map(detect_nuclei, images) meth4_times = %timeit -n 4 -r 1 -o centers = process_images4(images) # => 6.23 s ± 0 ns per loop (mean ± std. dev. of 1 run, 4 loops each)
Чего ждать? Почему это медленнее чем читать изображения с диска с Многоподобная
? Если вы прочитаете документацию для Многоподобная
Модуль тщательно, вы узнаете, что данные переданы работникам Бассейн
Должен быть сериализован через рассол
Анкет Этот этап сериализации создает некоторые вычислительные накладные расходы. Это означает, что для метода 2 мы были накаливания строк пути, но в этом случае мы марировали целые изображения.
Метод 5 – Многопроцессия. Бассейн с сериализацией исправить
К счастью, есть несколько способов избежать этой сериализации накладных расходов при использовании Многоподобная
Анкет В системах Mac и Linux мы можем воспользоваться тем, как операционная система обрабатывает вилки процесса, чтобы эффективно обрабатывать большие массивы в памяти (извините Windows 🤷♂️). Системы на основе UNIX используют копия на записи поведение для раздвоенных процессов. Сложная копия на записи означает, что раздвоенный процесс будет копировать данные только при попытке изменить общую виртуальную память. Все это происходит за кулисами, и копия на записи иногда называют неявный обмен Анкет
def _process_image_memory_fix(i): global images return detect_nuclei(images[i]) def process_images5(n): with mp.Pool() as pool: return pool.map(_process_image_memory_fix, range(n)) meth5_times = %timeit -n 4 -r 1 -o centers = process_images5(len(paths)) # => 5.31 s ± 0 ns per loop (mean ± std. dev. of 1 run, 4 loops each)
Хороший! Сейчас это немного быстрее, чем метод 2 (как мы могли изначально ожидали). Обратите внимание, что мы используем Глобальный
Заявление здесь, чтобы явно заявить, что эта функция использует картинки
массив определен во всем мире. Хотя это не требуется строго, я обнаружил, что было бы полезно указать, что _process_image_memory_fix
Функция зависит от некоторых изображения
массив доступен. Мы тогда карта
По всем индексам и позволяйте каждому процессу получить доступ к ему нужен изображение, индексируя в картинки
множество. Этот подход будет только раскачивать целые числа вместо самих изображений.
Полученные результаты
Давайте сравним среднее время выполнения всех 5 методов.
В целом, Многоподобная
резко сократило среднее время выполнения для этой задачи обнаружения ядер. Интересно, что наивный подход к многопроцессорной обработке с изображениями в памяти фактически увеличил среднее время выполнения. Воспользовавшись поведением копии на записи, мы смогли удалить значительные сериализационные накладные расходы, понесенные при сорвании целых изображений.
Чтобы показать больше наших результатов, давайте покажем случайную выборку из 16 изображений DAPI, отсортированных по количеству обнаруженных ядер в каждом изображении.
Похоже, что наша стратегия обнаружения простых ядер была эффективной и позволила нам организовать изображения DAPI в соответствии с наблюдаемой плотностью клеток в каждом поле зрения.
Вывод
От Spaceballs фильм
Я действительно надеюсь, что эта статья была полезна, поэтому, пожалуйста, дайте мне знать, если вам понравилось. Многоподобная
Методы, представленные здесь, помогли мне достичь смешной скорости при обработке изображений для нашего Скаутская бумага Анкет Возможно, в будущем мы сможем вернуться к этому примеру, но с ускорением графического процессора, чтобы наконец достичь Не смешная скорость Анкет
Бонус: использование стержней прогресса с многопрофильной. Бассейн
В вышеперечисленном Многоподобная
Примеры, у нас нет хорошего TQDM
индикатор. Мы можем получить один, если используем Pool.imap
Вместо этого, что полезно для продолжительных вычислений.
def _process_image(path): return detect_nuclei(io.imread(path)) def process_images_progress(paths): with mp.Pool() as pool: return list(tqdm(pool.imap(_process_image, paths), total=len(paths))) centers = process_images_progress(paths)
использованная литература
Мы использовали набор изображений BBBC021V1 [ Caie et al., Molecular Cancer Therapeutics, 2010 ], доступно в коллекции Broad BioImage Benchmark [ Ljosa et al., Природа Методы, 2012 ]
Доступность источника
Все исходные материалы для этой статьи доступны Здесь В моем блоге Github Repo.
Оригинал: “https://dev.to/jmswaney/speeding-up-scientific-computing-with-multiprocessing-in-python-3a43”