Как импортировать (загрузить) изображение в python?
Примеры того, как импортировать (загрузить) изображение в python:
Импорт изображения с помощью matplotlib
Чтобы импортировать изображение в python, одним из решений является использование matplotlib:
Code language: JavaScript (javascript)from matplotlib import image from matplotlib import pyplot as plt img = image.imread("fav.jpg")
Code language: PHP (php)print(type(img)) print(img.shape)
Code language: HTML, XML (xml)class 'numpy.ndarray'>
(300, 450, 3)
3 соответствует режиму RGB.
После этого можно вывести изображение с помощью imshow из matplotlib в виде графика.
Code language: CSS (css)plt.imshow(img) plt.show()

Импорт изображения с помощью Pillow
Другое решение – использовать Pillow
Code language: JavaScript (javascript)from PIL import Image from matplotlib import pyplot as plt img= Image.open("fav.jpg")
type(img)
не является массивом numpy:
Code language: CSS (css)PIL.JpegImagePlugin.JpegImageFile
Тем не менее, все еще можно вывести изображение на экран с помощью imshow
Code language: CSS (css)plt.imshow(img) plt.show()
Для преобразования img в матрицу numpy
Code language: JavaScript (javascript)import numpy as np img = np.asarray(img)
Работа с изображениями на Python
Тема сегодняшнего разговора — чему же научился Python за все годы своего существования в работе с изображениями. И действительно, кроме старичков родом из 1990 года ImageMagick и GraphicsMagick, есть современные эффективные библиотеки. Например, Pillow и более производительная Pillow-SIMD. Их активный разработчик Александр Карпинский (homm) на MoscowPython сравнил разные библиотеки для работы с изображениями на Python, представил бенчмарки и рассказал о неочевидных особенностях, которых всегда хватает. В этой статье расшифровка доклада, который поможет вам выбрать библиотеку под свое приложение, и сделать так, чтобы она работало максимально эффективно.
О спикере: Александр Карпинский работает в компании Uploadcare и занимается сервисом быстрой модификации изображений на лету. Участвует в разработке Pillow — популярной библиотеки для работы с изображениями на Python, развивает собственный форк этой библиотеки — Pillow-SIMD, который использует современные инструкции процессоров для наибольшей производительности.
Бэкграунд
Сервис модификации изображений в Uploadcare — это сервер, к которому приходит HTTP-запрос с идентификатором изображения и какими-то операциями, которые нужно выполнить клиенту. Сервер должен выполнить операции и как можно быстрее отдать ответ. В качестве клиента чаще всего выступает браузер.
Весь сервис можно описать как обертку вокруг графической библиотеки. Именно от качества, производительности и удобства использования графической библиотеки зависит качество всего проекта. Несложно догадаться, что в качестве графической библиотеки в Uploadcare используется Pillow.
Библиотеки
Кратко рассмотрим, какие вообще есть в Python библиотеки для работы с графикой, чтобы лучше понимать, о чем пойдет речь далее.
Pillow
Pillow — форк PIL (Python Imaging Library). Это очень старый проект, вышедший в 1995 году для Python 1.2. Можно представить, насколько он старый! В какой-то момент Python Imaging Library была заброшена, ее разработка прекратилась. Форк Pillow был сделан для того, чтобы устанавливать и собирать Python Imaging Library на современных системах. Постепенно количество изменений, которые нужны были людям в Python Imaging Library росло, и вышла Pillow 2.0, в которую была добавлена поддержка Python 3. Это можно считать началом отдельной жизни проекта Pillow.
Pillow представляет из себя нативный модуль для Python, половина кода написана на С, половина — на Python. Версии Python поддерживаются самые разнообразные: 2.7, 3.3+, PуPу, PуPуЗ.
Pillow-SIMD
Это мой форк Pillow, который выходит с мая 2016 года. SIMD означает Single Instruction, Multiple Data — подход, при котором процессор может выполнять большее количество действий на такт, используя современные инструкции.
Pillow-SIMD — это не форк в классическом понимании, когда проект начинает жить собственной жизнью. Это замена Pillow, то есть вы устанавливаете одну библиотеку вместо другой, ни строчки не меняете в своем исходном коде, и получаете большую производительность.
Pillow-SIMD можно собирать с инструкциями SSE4 (по умолчанию). Это набор инструкций, который есть практически во всех современных x86 процессорах. Также Pillow-SIMD можно собирать с набором инструкций AVX2. Этот набор инструкций есть, начиная с архитектуры Haswell, то есть примерно с 2013 года.
OpenCV
Другая библиотека для работы с изображениями в Python, о которой вы наверняка слышали — это OpenCV (Open Computer Vision). Работает с 2000 года. Биндинг на Python входит в комплект. Это означает, что биндинг постоянно актуален, не бывает рассинхронности между самой библиотекой и биндингом.
К сожалению, эта библиотека пока не поддерживается в PyPy, потому что OpenCV базируется на numpy, а numpy только недавно стал работать под PyPy, и в OpenCV поддержки PyPy все еще нет.
VIPS
Еще одна библиотека, на которую стоит обратить внимание — VIPS. Основная идея VIPS в том, что для работы с изображением не нужно загружать всё изображение в память. Библиотека может загружать какие-то маленькие кусочки, обрабатывать их и сохранять. Таким образом, для обработки гигапиксельных изображений не нужно тратить гигабайты памяти.
Это довольно старая библиотека — 1993 года, но она обогнала своё время. О ней долгое время было мало, что слышно, но в последнее время для VIPS стали появляться биндинги под разные языки, в том числе для Go, Node.js, Ruby.
Я долгое время хотел попробовать эту библиотеку, пощупать, но мне это не удавалось по очень глупой причине. Я не мог разобраться, как установить VIPS, потому что биндинг собирался очень сложно. Но теперь (в 2017 году) вышел биндинг pyvips от автора самого VIPS, с которым уже нет никаких проблем. Теперь установка и использование VIPS очень простая. Поддерживаются: Python 2.7, 3.3+, PуPу, PуPуЗ.
ImageMagick & GraphicsMagick
Если говорить о работе с графикой, то нельзя не упомянуть старичков — библиотеки ImageMagick и GraphicsMagick. Последняя изначально была форком ImageMagick с большей производительностью, но сейчас производительность у них, кажется, сравнялась. Других принципиальных отличий, насколько я знаю, между ними нет. Поэтому можно использовать любую, точнее, ту, которую вам удобнее использовать.
Это самые старые библиотеки из тех, о которых я сегодня упомянул (1990 год). За все это время было несколько биндингов для Python, и почти все из них благополучно умерли к настоящему моменту. Из тех, что можно использовать, остались:
- Биндинг Wand, который построен на ctypes, но уже тоже не обновляется.
- Биндинг pgmagick использует Boost.Python, поэтому компилируется очень долго и не работает в PyPy. Но, тем не менее, использовать его можно, я бы сказал, что он предпочтительнее, чем Wand.
Производительность
Когда мы говорим о работе с изображениями, первое, что нам интересно (по крайней мере мне) — это производительность, потому что иначе мы бы могли и на Python написать что-нибудь руками.
Производительность — это не такая простая штука. Нельзя просто сказать, что одна библиотека работает быстрее другой. В каждой библиотеке есть набор функций, и каждая функция работает с разной скоростью.
Соответственно, корректно говорить только о том, что производительность одной функции выше или ниже в конкретной библиотеке. Либо у вас есть приложение, которому нужен некий набор функциональности, и вы делаете бенчмарк именно под эту функциональность, и говорите, что для вашего приложения такая-то библиотека работает быстрее (медленнее).
Важно проверять результат
Когда вы делаете бенчмарки, очень важно смотреть на результат, который получается. Даже если с первого взгляда вы написали один и тот же код, это не значит, что он одинаковый.
Недавно в статье, где сравнивалась производительность Pillow и OpenCV, мне попался такой код:
from PIL import Image, ImageFilter.BoxBlur im.filter(ImageFilter.BoxBlur(3)) . import cv2 cv2.blur(im, ksize=(3, 3)) .
Вроде бы и там, и там, BoxBlur, и там, и там аргумент 3, но на самом деле результат разный. Потому что в Pillow (3) — это радиус размытия, а в OpenCV ksize=(3, 3) — это размер ядра, то есть, грубо говоря, диаметр. В данном случае корректное значение для OpenCV было бы 3*2+1, то есть (7, 7).
В чем проблема?
Почему вообще производительность — это проблема при работе с графикой? Потому что сложность любой операций зависит от нескольких параметров, и чаще всего сложность растет линейно с каждым из них. А если этих факторов, например, три, и сложность линейно зависит от каждого, то получается сложность в кубе.
Пример: Гауссово размытие в OpenCV.

Слева — радиус 3, справа — 30. Как видно, разница в скорости более чем в 10 раз.
Когда передо мной встала задача добавить размытие по Гауссу в мое приложение, меня не устраивало, что гипотетически может тратиться 900 мс на выполнение одной операции. Таких операций в приложении тысячи в минуту, и тратить столько времени на одну — нецелесообразно. Поэтому я изучил вопрос и реализовал в Pillow размытие по Гауссу, которое работает за константное время относительно радиуса. То есть на производительность Гауссова размытия влияет только размер картинки.
Но здесь главное не то, что что-то работает быстрее или медленнее.
Я хочу донести, что, когда вы строите какую-то систему, важно понимать, от каких параметров зависит сложность на выходе. Тогда вы сможете ограничить эти параметры или другими способами бороться с этой сложностью.
Наверное, самая частая операция, которую мы делаем с изображениями после их открытия, — это изменение размера (resize).

На графике представлена производительность (больше — лучше) разных библиотек для операции уменьшения изображения в 8 и в 1,25 раз.
Для PIL результат 17 Mpx/s означает, что фотографию с айфона (12 Mpx) можно уменьшить в 1,25 раз чуть меньше, чем за секунду. Такой производительности недостаточно для серьезного приложения, которое этих операций выполняет очень много.
Я стал оптимизировать производительность ресайза, и в Pillow 2.7 мне удалось добиться двукратного прироста производительности, а в Pillow 4.3 — трехкратного (на данный момент актуальна версия Pillow 5.3, но производительность ресайза в ней та же).
Но операция ресайза — это такая штука, которая очень хорошо ложится на SIMD. Она подходит к single instruction, multiple data, и поэтому в текущей версии Pillow-SIMD мне удалось в 19 раз увеличить скорость ресайза по сравнению с оригинальной Python Imaging Library при использовании тех же самых ресурсов.
Это существенно выше производительности ресайза в OpenCV. Но сравнение это не совсем корректное, потому что в OpenCV используется чуть менее качественный способ ресайза бокс-фильтром, а в Pillow-SIMD ресайз реализован с помощью сверток.
Это неполный список тех операций, которые ускорены в Pillow-SIMD по сравнению с обычной Pillow.
- Ресайз: от 4 до 7 раз.
- Размытие: 2,8 раз.
- Применение ядра 3×3 или 5×5: 11 раз.
- Умножение и деление на альфа-канал: 4 и 10 раз.
- Альфа-композиция: 5 раз.

Оказалось, что Pillow-SIMD на этом наборе работает в 2 раза быстрее, чем Pillow. В самом конце находится Wand (напомню, что это ImageMagick).
Но меня заинтересовало другое — почему такие низкие результаты у OpenCV и VIPS, ведь это библиотеки, которые тоже разработаны с прицелом на производительность? Оказалось, что в случае OpenCV, та бинарная сборка OpenCV, которая ставится с помощью pip, собрана с медленным кодеком JPEG (автор сборки был уведомлен, на 2018 год эта проблема уже решена). Она собрана с libjpeg, в то время как в большинстве систем, по крайней мере debian-based, используется libjpeg-turbo, которая работает в несколько раз быстрее. Если собрать OpenCV из исходников самому, то производительность будет больше.
В случае VIPS ситуация другая. Я связался с автором VIPS, показал ему этот бенчмарк, мы с ним довольно долго и плодотворно переписывались. После чего автор VIPS нашел несколько мест в самом VIPS, где исполнение было не по оптимальному маршруту, и исправил их.
Вот что будет с производительностью, если собрать OpenCV из исходников текущей версии, а VIPS — из master, который уже есть сейчас.
Даже если вы нашли какой-то бенчмарк, еще не факт, что все будет работать именно с такой скоростью именно на вашей машине.
Набор бенчмарков
Все бенчмарки, о которых я рассказал, можно найти на странице с результатами. Это отдельный мини-проект, где я пишу бенчмарки, которые мне самому нужны для разработки Pillow-SIMD, запускаю их и выкладываю результаты.
На GitHub есть проект с фреймворками для тестирования, где каждый может предложить свои бенчмарки или поправить существующие.
Параллельная работа
До сих пор я говорил о производительности в чистом виде, то есть на одно ядро процессора. Но нам всем давно доступны системы с бо́льшим количеством ядер, и хотелось бы их утилизировать. Здесь я должен сказать, что на самом деле Pillow — это единственная из всех рассматриваемых библиотек, которая не использует распараллеливание задач. Я сейчас постараюсь объяснить, почему так происходит. Все остальные библиотеки в том или ином виде его используют.
Метрики производительности
С точки зрения производительности нам интересны 2 параметра:
- Реальное время выполнения одной операции. Есть операция (либо последовательность операций), и вам интересно, за какое реальное время (wall clock) выполнится эта последовательность. Этот параметр важен на десктопе, где есть пользователь, который отдал команду и ждет результат.
- Пропускная способность всей системы (потока операций). Когда у вас есть набор операций, который идет постоянно, либо много независимых операций, и вам важна скорость обработки этих операций на вашем железе. Эта метрика более важна на сервере, где клиентов много, и нужно обслужить их всех. Время обслуживания одного клиента, конечно, важно, но чуть меньше, чем общая пропускная способность.
Способы параллельной работы
1. На уровне приложения, когда вы на уровне приложения решаете, что операции обрабатываются в разных потоках. При этом реальное время выполнения одной операции не меняется, потому что по-прежнему одно ядро занимается одной последовательностью операций. Пропускная способность системы растет пропорционально количеству ядер, то есть очень хорошо.
2. На уровне графических операций — то, что как раз есть в большинстве графических библиотек. Когда графическая библиотека получает какую-то операцию, она внутри себя создает необходимое количество потоков, разбивает одну операцию на несколько более мелких, и выполняет их. При этом реальное время выполнения уменьшается — одна операция выполняется быстрее. Но пропускная способность растет далеко не линейно от количества ядер. Есть операции, которые не параллелятся, и яркий пример — это декодирование PNG-файлов — его никак нельзя распараллелить. Кроме того, есть накладные расходы на создание потоков, разбиение задач, которые тоже не дают пропускной способности расти линейно.
3. На уровне команд процессора и данных. Мы специальным образом подготавливаем данные и используем специальные команды, для того чтобы процессор работал с ними быстрее. Это подход SIMD, который, собственно, и используется в Pillow-SIMD. Реальное время выполнения уменьшается, пропускная способность растет — это беспроигрышный вариант.
Как совместить параллельную работу

Если мы хотим совместить как-то параллельную работу, то SIMD хорошо совмещается с распараллеливанием внутри операции, и SIMD хорошо совмещается с распараллеливанием внутри приложения.
Но распараллеливание внутри приложения и внутри операции друг с другом не совмещаются. Если вы попробуете это сделать, то получите минусы от обоих подходов. Реальное время выполнения операции будет таким же, каким оно и было на одном ядре, а пропускная способность системы вырастет, но не линейно относительно количества ядер.
Многопоточность
Если уж мы заговорили про потоки — мы все пишем на Python и знаем, что в нем есть GIL, который не дает выполняться двум потокам одновременно. Python — это строго однопоточный язык.
Конечно, это не правда, потому что GIL на самом деле не дает выполняться двум потокам на Python, а если код написан на другом языке, и во время своей работы не использует внутренние структуры Python, то этот код может отпускать GIL и таким образом освобождать интерпретатор под другие задачи.
Многие библиотеки для работы с графикой освобождают GIL во время своей работы, в том числе Pillow, OpenCV, pyvips, Wand. Не освобождает только одна — pgmagick. То есть вы можете спокойно создавать потоки под выполнение каких-то операций, и это будет работать параллельно с остальным кодом.
Но возникает вопрос: сколько создавать потоков?
Если мы будем создавать бесконечное количество потоков под каждую задачу, которая у нас есть, то они просто займут всю память и весь процессор, — мы не получим никакой эффективной работы. Я сформулировал специальное правило.
Для продуктивной работы нужно создавать не более N + 1 воркеров, где N — количество ядер или потоков процессора на машине, а воркер — это процесс или поток, занятый обработкой.
Лучше всего использовать именно процессы, потому что даже в рамках одного интерпретатора есть узкие места и накладные расходы.
Например, у нас в приложении используется N + 1 instance Tornado, балансировку между которыми осуществляет ngnix. Если упомянуто Tornado, то давайте поговорим об асинхронной работе.
Асинхронная работа
То время, которое графическая библиотека занимается собственно полезной работой — обработкой изображений — можно и нужно использовать для ввода/вывода, если они есть у вас в приложении. Асинхронные фреймворки здесь очень в тему.
Но есть проблема — когда мы вызываем какую-то обработку, она вызывается синхронно. Даже если библиотека отпускает в этот момент GIL, все равно Event Loop блокируется.
@gen.coroutine def get(self, *args, **kwargs): im = process_image(. ) .
К счастью, эту проблему очень легко решить созданием ThreadPoolExecutor с одним тредом, на котором запускается обработка изображения. Этот вызов уже происходит асинхронно.
@run_on_executor(executor=ThreadPoolExecutor(1)) def process_image(self, . @gen.coroutine def get(self, *args, **kwargs): im = yield process_image(. ) .
По сути здесь создается очередь с одним воркером, который выполняет именно графические операции, а Event Loop не блокируется и спокойно работает параллельно в другом потоке.
Ввод/Вывод
Еще одна тема, которую я бы хотел затронуть в рамках разговора о графических операциях — это ввод/вывод. Дело в том, что мы редко создаем какие-то изображения с помощью графической библиотеки. Чаще всего мы открываем изображения, которые пришли к нам от пользователей в виде закодированных файлов (JPEG, PNG, BMP, TIFF и пр.).
Соответственно, графическая библиотека для построения хорошего приложения должна обладать некоторыми плюшками по вводу/выводу именно из файлов.
Ленивая загрузка
Первая такая плюшка — это ленивая загрузка. Если, например, в Pillow вы открываете изображение, то в этот момент не происходит декодирования картинки. Вам возвращается объект, который выглядит, как будто изображение уже загружено и работает. Вы можете посмотреть его свойства и решить на основании свойств этого изображения, готовы ли вы с ним работать дальше, не загрузил ли вам пользователь, например, гигапиксельную картинку для того, чтобы сломать ваш сервис.
>>> from PIL import Image >>> %time im = Image.open() Wall time: 1.2 ms >>> im.mode, im.size ('RGB', (2152, 1345))
Если вы принимаете решение, что дальше работаете, то с помощью явного или неявного вызова load происходит декодирование этого изображения. Уже в этот момент выделяется нужное количество памяти.
>>> from PIL import Image >>> %time im = Image.open() Wall time: 1.2 ms >>> im.mode, im.size ('RGB', (2152, 1345)) >>> %time im.load() Wall time: 73.6 ms
Режим битых картинок
Вторая плюшка, которая нужна при работе с пользовательским контентом — это режим битых картинок. Файлы, которые мы получаем от пользователей, очень часто содержат какие-то несоответствия с форматом, в котором они закодированы.
Эти несоответствия бывают по разным причинам. Иногда это ошибки передачи по сети, иногда — просто какие-то кривые кодеки, которыми кодировалось изображение. По умолчанию Pillow, когда видит изображения, которые не соответствуют формату до конца, просто кидает исключение.
from PIL import Image Image.open('trucated.jpg').save('trucated.out.jpg') IOError: image file is truncated (143 bytes not processed)
Но пользователь же не виноват в том, что у него картинка битая, он все равно хочет получить результат. К счастью, у Pillow есть режим битых картинок. Мы меняем одну настройку, и Pillow старается по максимуму игнорировать все ошибки декодирования, которые есть в изображении. Соответственно, пользователь видит хотя бы что-то.
from PIL import Image, ImageFile ImageFile.LOAD_TRUNCATED_IMAGES = True Image.open('trucated.jpg').save('trucated.out.jpg')

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

В таблице выше я собрал все, что касается ввода/вывода в тех библиотеках, о которых я рассказываю. В частности, я посчитал количество кодеков различных форматов, которые есть в библиотеках. Получилось, что в OpenCV их меньше всех, в ImageMagick — больше всех. Похоже, что в ImageMagick можно открыть вообще любое изображение, которое вам встретится. У VIPS 12 собственных кодеков, но VIPS может использовать ImageMagick в качестве промежуточного звена. Я не проверял, как это работает, надеюсь, что бесшовно.
В Pillow 17 кодеков. Это сейчас единственная библиотека, в которой нет автоповорота EXIF. Но сейчас это небольшая проблема, потому что можно прочитать EXIF самому и повернуть изображение в соответствии с ним. Это вопрос небольшого сниппета, который легко гуглится и занимает максимум в 20 строчек.
Особенности OpenCV
Если посмотреть на эту таблицу внимательно, то видно, что в OpenCV на самом деле не все так хорошо с вводом/выводом. В нем меньше всего кодеков, нет ленивой загрузки и нельзя прочитать EXIF и цветовой профиль.
Но это не все. На самом деле у OpenCV есть еще особенности. Когда мы просто открываем какую-то картинку, то вызов cv2.imread(filename) поворачивает JPEG-файлы в соответствии с EXIF (см. таблицу), но при этом игнорирует альфа-канал у PNG-файлов — довольно странное поведение!
К счастью, в OpenCV есть флаг: cv2.imread(filename, flags=cv2.IMREAD_UNCHANGED) .
Если указать флаг IMREAD_UNCHANGED, то OpenCV оставляет альфа-канал у PNG-файлов, но перестает поворачивать JPEG-файлы в соответствии с EXIF. То есть один и тот же флаг влияет на два совершенно разных свойства. Как видно из таблицы, у OpenCV нет возможности прочитать EXIF, и получается, что в случае с этим флагом невозможно повернуть JPEG вообще.
Что делать, если вы заранее не знаете, какого формата у вас изображение, и вам нужны и альфа-канал у PNG, и автоповорот у JPEG? Ничего не сделать — OpenCV так не работает.
Причина, почему в OpenCV такие проблемы, кроется в названии этой библиотеки. В ней очень много функциональности для компьютерного зрения и анализа изображений. По сути, OpenCV рассчитана на работу с верифицированными источниками. Это, например, камера наружного наблюдения, которая раз в секунду скидывает изображения, и делает это в течение 5 лет в одном и том же формате и одном и том же разрешении. Здесь не нужна вариативность в вопросе ввода/вывода.
Людям, которым нужна функциональность OpenCV, не сильно нужна функциональность работы с пользовательским контентом.
Но что делать, если в вашем приложении все-таки нужна функциональность работы с пользовательским контентом, и в то же время нужна вся мощь OpenCV по обработке и статистике?

Решение в том, что нужно совмещать библиотеки. Дело в том, что OpenCV построен на базе numpy, и в Pillow есть все средства для того, чтобы экспортировать изображения из Pillow в массив numpy. То есть мы экспортируем массив numpy, и OpenCV может дальше работать с этим изображением, как со своим собственным. Это делается очень легко:
import numpy from PIL import Image . pillow_image = Image.open(filename) cv_image = numpy.array(pillow_image)
Дальше, когда мы делаем магию с помощью OpenCV (обработку), мы вызываем еще один метод Pillow и обратно импортируем из OpenCV картинку в формат Pillow. Соответственно, можно снова пользоваться вводом/выводом.
import numpy from PIL import Image . pillow_image = Image.fromarray(cv_image, "RGB") pillow_image.save(filename)
Таким образом получается, что мы пользуемся вводом/выводом от Pillow, и процессингом от OpenCV, то есть берем лучшее из двух миров.
Надеюсь, это поможет вам построить нагруженное приложение для работы с графикой.
Узнать некоторые другие секреты разработки на Python, перенять бесценный и местами неожиданный опыт, а главное обсудить свои задачи можно будет уже совсем скоро на Moscow Python Conf++. Например, обратите внимание на такие имена и темы в расписании.
- Donald Whyte с историей о том, как с помощью популярных библиотек, хитрости и коварства делать математику в 10 раз быстрее, а код — понятным и поддерживаемым.
- Андрей Попов про сбор огромного количества данных и их анализ на наличие угроз.
- Ефрем Матосян в своем докладе «Make Python fast again» расскажет, как увеличить производительность демона, обрабатывающего сообщения из шины.
- Блог компании Конференции Олега Бунина (Онтико)
- Веб-разработка
- Python
- Обработка изображений
Как вставить одно изображение в другое используя Pillow

Метод paste() из библиотеки Pillow, предоставляет возможность вставки одного изображения в другое изображение.
В данной статье будут рассматриваться следующие аспекты:
- Обычная вставка одного изображения в другое изображение;
- Вставка изображения с указанием координат для вставки;
- Использование маски.
Импортируем модуль Image из библиотеки PIL и открываем базовое (фоновое) изображение, куда будет производиться вставка другого изображения.
Установка библиотеки Pilllow
pip install Pillow
Модули ImageDraw и ImageFilter мы будем использовать для создания маски. Вы можете пропустить этот момент, если вам нужна простая вставка изображения.
from PIL import Image
im1 = Image . open ( ‘guido-van-rossum.jpg’ )
im2 = Image . open ( ‘fon.jpg’ )
Сохраните эти два изображения:
- guido-van-rossum.jpg (создатель языка программирования Python);
- fon.jpg (Фоновое изображение).
![]() |
![]() |
Обычная вставка изображения через PIL
Вызываем метод paste() от изображения фона и указываем картинку для вставки.
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Telegram Чат & Канал
Вступите в наш дружный чат по Python и начните общение с единомышленниками! Станьте частью большого сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
По умолчанию изображение вставляется в верхней левой части фоновой картинки.
from PIL import Image
im1 = Image . open ( ‘fon.jpg’ )
im2 = Image . open ( ‘guido-van-rossum.jpg’ )
im1 . paste ( im2 )
im1 . save ( ‘fon_pillow_paste.jpg’ , quality = 95 )
im1 . close ( )
im2 . close ( )
Результат выполнения данного кода:

Метод paste() переписывает объект фонового изображение, поэтому, если вы хотите сохранить оригинальную картинку из переменной im1, то скопируйте объект уже открытого изображения через метод copy() .
from PIL import Image
im1 = Image . open ( ‘fon.jpg’ )
im2 = Image . open ( ‘guido-van-rossum.jpg’ )
back_im = im1 . copy ( )
back_im . paste ( im2 )
back_im . save ( ‘fon_pillow_paste.jpg’ , quality = 95 )
im1 . close ( )
im2 . close ( )
Координаты для вставки изображения
Позиция для вставки картинки указывается с помощью кортежа (координата х верхнего левого угла, координата y верхнего левого угла) во втором параметре box .
from PIL import Image
im1 = Image . open ( ‘fon.jpg’ )
im2 = Image . open ( ‘guido-van-rossum.jpg’ )
im1 . paste ( im2 , ( 100 , 50 ) )
im1 . save ( ‘fon_pillow_paste_pos.jpg’ , quality = 95 )
im1 . close ( )
im2 . close ( )
Ниже мы увидим готовый результат. Изображение с Гвидо Ван Россумом была вставлена поверх другого изображения со смещением слева на 100 пикселей и смещение сверху на 50 пикселей.

Если встраиваемое изображение выходит за пределы фоновой картинки, то лишняя область просто исчезнет за пределы фонового изображения.
from PIL import Image
im1 = Image . open ( ‘fon.jpg’ )
im2 = Image . open ( ‘guido-van-rossum.jpg’ )
im1 . paste ( im2 , ( 450 , 100 ) )
im1 . save ( ‘fon_pillow_paste_out.jpg’ , quality = 95 )
im1 . close ( )
im2 . close ( )

Использование маски в Pillow
Если маска указывается в качестве третьего параметра mask , встраиваемое изображение можно обрезать и вставить не только как прямоугольник, но и как любые другие формы в зависимости от формы этой самой маски.
Изображение, которое будем использовать как маску должно быть такого же размера, что и встраиваемая картинка, и у него должны быть следующие три типа режима mode :
- 1 : 1-битная картинка (бинарное изображение);
- L : 8-битое черно-белое изображение;
- RGBA : Картинка с альфа каналом (прозрачная).
Когда маска является 8-битной (черно-белой mode=’L’ ), фон виден, когда значение маски равно 0 (черный), а встраиваемой картинки — 255 (белый). Для других значений обе картинки сливаются в соответствии со значением.
Нарисуем белый круг на черном фоне с помощью модуля ImageDraw для генерации маски.
from PIL import Image , ImageDraw
mask_im = Image . new ( «L» , im2 . size , 0 )
draw = ImageDraw . Draw ( mask_im )
draw . ellipse ( ( 100 , 10 , 270 , 180 ) , fill = 255 )
mask_im . save ( ‘data/dst/mask_circle.jpg’ , quality = 95 )

Подробнее о рисовании с помощью Pillow можно прочесть в данной статье — Рисование круга, прямоугольника, линии в Python
Процесс вставки осуществляется через использование данной картинки как маски.
Что произойдет с данной маской?
- Все темные пиксели маски будут использованы как метки для удаления этого пикселя из нашей картинки с Гвидо Ван Россумом;
- Светлая часть маски оставит пиксели как есть;
- Если пиксели маски серые (не совсем белые и не совсем черные), то данная часть будет прозрачной только на несколько процентов.
from PIL import Image , ImageDraw
im1 = Image . open ( ‘fon.jpg’ )
im2 = Image . open ( ‘guido-van-rossum.jpg’ )
mask_im = Image . new ( «L» , im2 . size , 0 )
draw = ImageDraw . Draw ( mask_im )
draw . ellipse ( ( 100 , 10 , 270 , 180 ) , fill = 255 )
im1 . paste ( im2 , ( 0 , 0 ) , mask_im )
im1 . save ( ‘fon_pillow_paste_mask_circle.jpg’ , quality = 95 )
im1 . close ( )
im2 . close ( )
mask_im . close ( )
Вот и готовый результат. Как мы видим, маска удалила из нашей картинки все участки где у нее был черный пиксель.

Когда маска размывается с помощью модуля ImageFilter , границы можно смешивать и вставлять.
from PIL import Image , ImageDraw , ImageFilter
im1 = Image . open ( ‘fon.jpg’ )
im2 = Image . open ( ‘guido-van-rossum.jpg’ )
mask_im = Image . new ( «L» , im2 . size , 0 )
draw = ImageDraw . Draw ( mask_im )
draw . ellipse ( ( 100 , 10 , 270 , 180 ) , fill = 255 )
mask_im_blur = mask_im . filter ( ImageFilter . GaussianBlur ( 10 ) )
im1 . paste ( im2 , ( 0 , 0 ) , mask_im_blur )
im1 . save ( ‘fon_pillow_paste_mask_circle_blur.jpg’ , quality = 95 )
im1 . close ( )
im2 . close ( )
mask_im . close ( )
У нас получилась такая маска:

Вот и готовый результат с применением данной маски:

Модуль ImageDraw не ограничивается только геометрическими фигурами, маски могут вставляться в любой форме.
Попробуйте использовать вот такую картинку с черно-белой бабочкой.

После того, как метод open() откроет картинку, она приспособится к размеру встраиваемого изображения через использование метода resize() , а режим конвертируется в ‘L’ (черно-белое) через метод convert() .
from PIL import Image
im1 = Image . open ( ‘fon.jpg’ )
im2 = Image . open ( ‘guido-van-rossum.jpg’ )
mask_im = Image . open ( ‘butterfly.jpg’ ) . resize ( im2 . size ) . convert ( ‘L’ )
im1 . paste ( im2 , ( 100 , 100 ) , mask_im )
im1 . save ( ‘fon_pillow_paste_mask_butterfly.jpg’ , quality = 95 )
im1 . close ( )
im2 . close ( )
mask_im . close ( )

Это не самый лучший пример, форма довольно сложная, и случай для ее использования сложно придумать. А вот маскам со звездочками и сердечками можно найти применение.

Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»
Обработка изображений с помощью библиотеки Python Pillow

Данный туториал является переводом статьи, написанной Stephen Grupetta. Все изображения и коды скопированы без изменений. В конце вы найдете примечания относительно данной информации, а также ссылку на github с работающим кодом. Если код, приведенный автором не запускается, переходите в примечания и, возможно, сможете найти решение вашей ошибки.
Оглавление
- Основные операции с изображениями в библиотеке Python Pillow
- Модуль Image и класс Image в Pillow
- Основные манипуляции с использованием модуля Image
- Градации и режимы модуля Image в библиотеке Python Pillow
- Фильтры изображений с использованием Convolution Kernels (ядра свертки)
- Размытие изображения, повышение резкости и сглаживание
- Обнаружение краев, приданием им четкости и тиснение
- Порог изображения
- Сжатие(erosion) и расширение(dilation)
- Сегментация изображения с использованием порогового значения
- Наложение изображений с использованием Image.paste()
- Создание водяного знака
- Использование NumPy для вычитания изображений друг из друга (поиск разницы)
- Использование NumPy для создания изображений
- Создание анимации
Когда вы смотрите на изображение, вы видите на нем предметы и людей. Однако, когда вы программно читаете изображение с помощью Python или любого другого языка, компьютер видит массив чисел. В этом руководстве вы узнаете, как оперировать изображениями и выполнять базовую обработку с помощью библиотеки Python Pillow.
Pillow и его предшественник PIL — это оригинальные библиотеки Python для работы с изображениями. Несмотря на то, что существуют другие библиотеки Python для обработки изображений, Pillow остается важным инструментом для понимания и работы в целом.
Для оперирования и обработки изображений Pillow предоставляет инструменты, аналогичные тем, которые можно найти в программном обеспечении, таком как Photoshop. Некоторые из более современных библиотек обработки изображений Python построены на основе Pillow и часто предоставляют более продвинутую функциональность.
В этом уроке вы узнаете, как:
- Читать изображения с помощью Pillow
- Выполнять основные операции с изображениями
- Использовать Pilow для обработки изображений
- Используйте NumPy с Pillow для дальнейшей обработки
- Создавать анимации с помощью Pillow
В этом руководстве представлен обзор возможностей библиотеки Python Pillow с помощью распространенных методов. Как только вы освоитесь в использовании этих методов, вы сможете использовать документацию Pillow для изучения остальных методов в библиотеке. Если вы никогда раньше не работали с изображениями в Python, это отличная возможность сразу приступить!
В данном руководстве вы будете использовать несколько изображений, которые сможете загрузить из репозитория. Теперь вы готовы начать работу с Pillow.
Основные операции с изображениями в библиотеке Python Pillow
Библиотека Python Pillow является ответвлением более старой библиотеки под названием PIL. PIL расшифровывается как Python Imaging Library, и она позволяла Python работать с изображениями. PIL приостановила работу в 2011 году и поддерживается только Python 2. Если использовать описание разработчиков, то Pillow — это удобный «переходник» PIL, который поддерживает жизнь библиотеки и включает поддержку Python 3.
В Python есть много модулей для работы с изображениями и их обработки. Если вы хотите работать с изображениями напрямую, оперируя их пикселями, вы можете использовать NumPy и SciPy. Другими популярными библиотеками для обработки изображений являются OpenCV, scikit-image и Mahotas. Некоторые из этих библиотек быстрее и мощнее, чем Pillow.
Однако Pillow остается важным инструментом для работы с изображениями. Она предоставляет функции обработки, аналогичные тем, которые можно найти в программном обеспечении Photoshop. Pillow часто является предпочтительным вариантом для задач обработки изображений высокого уровня, которые не требуют более продвинутых навыков обработки.
Pillow также часто используется для исследовательской работы при работе с изображениями. Тут есть то преимущество, что она широко используется сообществом Python, и её не так тяжело изучить, как некоторые другие библиотеки обработки изображений.
Вам нужно будет установить библиотеку, прежде чем вы сможете ее использовать. Вы можете установить Pillow с помощью pip в виртуальной среде:
PS> python -m venv venv PS> .\venv\Scripts\activate (venv) PS> python -m pip install Pillow$ python -m venv venv $ source venv/bin/activate (venv) $ python -m pip install PillowТеперь, когда вы установили пакет, вы готовы начать знакомство с библиотекой Python Pillow и выполнять основные операции с изображениями.
Модуль Image и класс Image в Pillow
Основным классом, определенным в Pillow, является класс Image. Когда вы читаете изображение с помощью Pillow, оно сохраняется как объект типа Image.
Для написания программы в этом разделе вам понадобится файл изображения с именем building.jpg, которое вы можете найти в репозитории изображений для данного руководства.
Вы можете поместить этот файл изображения в корневую папку проекта, в котором работаете. При изучении изображений с помощью Pillow лучше всего использовать интерактивную среду REPL. Начните с открытия изображения, которое вы только что скачали:
>>> from PIL import Image >>> filename = "buildings.jpg" >>> with Image.open(filename) as img: . img.load() . >>> type(img) >>> isinstance(img, Image.Image) TrueВы можете ожидать импорта из Pillow, а не из PIL. В конце концов, вы установили Pillow, а не PIL. Однако Pillow — это ответвление библиотеки PIL. Поэтому вам все равно придется использовать PIL при импорте в ваш код.
Вы вызываете функцию open() для чтения изображения из файла и .load() для чтения в память, чтобы теперь файл можно было закрыть. Вы используете оператор with для создания менеджера контекста, чтобы обеспечить закрытие файла после всех операций. В этом примере объект представляет собой тип изображения JPEG, который является подклассом класса Image , что подтверждается вызовом isinstance() . Обратите внимание, что и класс, и модуль, в котором определен класс, имеют одно и то же имя Image . Вы можете отобразить изображение, используя .show() :
img.show()Метод .show() сохраняет изображение как временный файл и отображает его, используя собственное программное обеспечение вашей операционной системы для работы с изображениями. Когда вы запустите приведенный выше код, вы увидите следующее изображение:

В некоторых системах вызов .show() блокирует REPL, пока вы не закроете изображение. Это зависит от операционной системы и программного обеспечения для просмотра изображений по умолчанию, которое вы используете.
Вам необходимо ознакомиться с тремя ключевыми свойствами при работе с изображениями в библиотеке Python Pillow. Вы можете изучить их, используя атрибуты класса Image .format , .size и .mode :
>>> img.format 'JPEG' >>> img.size (1920, 1273) >>> img.mode 'RGB'format показывает, с каким типом изображения вы работаете. В данном случае — JPEG. size показывает ширину и высоту изображения. mode (режим) этого изображения — «RGB».
Часто вам может понадобиться обрезать и изменить размер изображения. Класс Image имеет два метода, которые можно использовать для выполнения этих операций: . crop() и . resize() :
>>> cropped_img = img.crop((300, 150, 700, 1000)) >>> cropped_img.size (400, 850) >>> cropped_img.show() >>> low_res_img = cropped_img.resize( . (cropped_img.width // 4, cropped_img.height // 4) . ) >>> low_res_img.show()Аргумент . crop() должен состоять из четырех элементов, определяющих левый, верхний, правый и нижний края области, которую вы хотите обрезать. Система координат, используемая в Pillow, назначает координаты (0, 0) пикселю в верхнем левом углу. Это та же система координат, которая обычно используется для двумерных массивов. Кортеж из четырех элементов представляет собой следующую область изображения:

Новое изображение, которое . crop() возвращает в приведенном выше коде, имеет размер 400×850 пикселей. На обрезанном изображении видно только одно из зданий с исходной картинки:

В приведенном выше коде вы также изменяете разрешение обрезанного изображения с помощью .resize(), которому требуется кортеж в качестве обязательного аргумента. Данный кортеж определяет новую ширину и высоту изображения в пикселях.
В приведенном выше примере вы устанавливаете новую ширину и высоту в четверть их исходных значений, используя оператор деления без остатка (//) и атрибуты изображения .width и . height . Последний вызов show() отображает обрезанное изображение с измененным размером:
Есть дополнительные необязательные параметры, которые можно использовать с . resize() для управления тем, как изображение масштабирутся. В качестве альтернативы вы можете использовать аналогичный метод масштабирования, используя .reduce() :
low_res_img = cropped_img.reduce(4)Аргумент определяет коэффициент, на который вы уменьшаете масштаб изображения. Если вы хотите установить максимальный размер, а не коэффициент масштабирования, вы можете использовать .thumbnail() . Размер эскиза будет меньше или равен установленному вами размеру.
Примечание. Метод .thumbnail() изменяет объект изображения, но не возвращает новый объект. Однако методы . crop() , .resize() и . reduce() возвращают новый объект изображения. Не все методы в библиотеке Pillow ведут себя одинаково.
Когда вы довольны возвращенным изображением, вы можете сохранить любой из объектов Image в файл, используя . save() :
cropped_img.save("cropped_image.jpg") low_res_img.save("low_resolution_cropped_image.png")Как только вы вызываете метод, он создает файлы изображений в папке вашего проекта. В этом примере одно из изображений является изображением в формате JPEG, а другое — изображением в формате PNG. Расширение, которое вы используете в качестве имени файла, автоматически определяет формат файла. Также вы можете указать формат в качестве дополнительного необязательного аргумента.
Основные манипуляции с использованием модуля Image
Вы можете оперировать изображением не только с помощью обрезки и изменения размера. Другим распространенным методом является поворот или отражение . Вы можете использовать метод .transpose() для некоторых преобразований. Продолжайте с той же сессией REPL, которую вы начали в предыдущем разделе:
converted_img = img.transpose(Image.FLIP_TOP_BOTTOM) converted_img.show()Этот код отображает следующее изображение:

Есть семь опций, которые вы можете передать в качестве аргументов .transpose():
- Image.FLIP_LEFT_RIGHT : переворачивает изображение слева направо, в результате чего получается зеркальное отображение.
- Image.FLIP_TOP_BOTTOM : переворачивает изображение сверху вниз.
- Image.ROTATE_90 : поворачивает изображение на 90 градусов против часовой стрелки.
- Image.ROTATE_180 : поворачивает изображение на 180 градусов.
- Image.ROTATE_270 : изображение поворачивается на 270 градусов против часовой стрелки, что соответствует повороту на 90 градусов по часовой стрелке.
- Image.TRANSPOSE : перемещает строки и столбцы, используя верхний левый пиксель в качестве источника, при этом верхний левый пиксель в перемещенном изображении такой же, как и в исходном изображении.
- Image.TRANSVERSE : перемещает строки и столбцы, используя нижний левый пиксель в качестве источника, при этом нижний левый пиксель остается фиксированным относительно исходной и измененной версиями.
Все параметры поворота, представленные выше, определяют повороты с шагом в 90 градусов. Если вам нужно повернуть изображение на другой угол, вы можете использовать .rotate() :
rotated_img = img.rotate(45) rotated_img.show()Этот метод поворачивает изображение на 45 градусов против часовой стрелки, возвращая следующее изображение:

Данный объект Image имеет тот же размер, что и исходное изображение. Поэтому на дисплее отсутствуют углы картинки. Вы можете изменить это поведение с помощью расширенного именованного параметра:
rotated_img = img.rotate(45, expand=True) rotated_img.show()Этот метод возвращает увеличенное изображение, полностью содержащее повернутое изображение:

Вы можете дополнительно настроить вращение с другими параметрами. Теперь вы изменили размер и ориентацию картинки. В следующем разделе вы узнаете о различных типах изображений в библиотеке Python Pillow.
Градации и режимы модуля Image в библиотеке Python Pillow
Изображение представляет собой двумерный массив пикселей, где каждый пиксель соответствует цвету. Каждый пиксель может быть представлен одним или несколькими значениями. Например, в изображении RGB каждый пиксель представлен тремя значениями, соответствующими значениям красного, зеленого и синего.
Таким образом, объект Image для изображения RBG содержит три параметра, по одному для каждого цвета. Изображение RGB размером 100×100 пикселей представлено массивом значений 100x100x3.
Изображения RGBA также включают значение альфа-параметра, которое содержит информацию о прозрачности каждого пикселя. Изображение RGBA имеет четыре режима, по одному для каждого из цветов и четвертый, содержащую альфа-значения. Каждый режим имеет те же размеры, что и размеры изображения. Таким образом, RGBA-изображение размером 100×100 пикселей представляется массивом значений 100x100x4.
Режим изображения описывает, с каким типом изображения вы работаете. Pillow поддерживает большинство стандартных режимов, включая черно-белый (двоичный), оттенки серого, RGB, RGBA и CMYK. Вы можете найти полный список поддерживаемых режимов в документации Pillow. Также вы можете узнать, сколько слоев(режимов) в объекте изображения, используя метод .getbands(), и вы можете конвертировать между режимами, используя .convert() . Теперь вы будете использовать изображение с именем «strawberry.jpg» (изображение предоставлено) из репозитория изображений для этого урока:

Режим этого изображения также RGB. Вы можете преобразовать это изображение в другие режимы. Данный код использует тот же сеанс REPL, который вы использовали в предыдущих разделах:
>>> filename = "strawberry.jpg" >>> with Image.open(filename) as img: . img.load() . >>> cmyk_img = img.convert("CMYK") >>> gray_img = img.convert("L") # Grayscale >>> cmyk_img.show() >>> gray_img.show() >>> img.getbands() ('R', 'G', 'B') >>> cmyk_img.getbands() ('C', 'M', 'Y', 'K') >>> gray_img.getbands() ('L',)Вы вызываете . convert() дважды, чтобы преобразовать изображение RGB в версию CMYK и в версию в градациях серого. Изображение CMYK похоже на исходное изображение, но закодировано с использованием режима, характерного для печатных материалов, а не для цифровых дисплеев. Преобразование в оттенки серого дает следующий результат:

Выходные данные вызовов . getbands() подтверждают наличие трех слоев в изображении RGB, четырех слоев в изображении CMYK и одного слоя в изображении в градациях серого.
Вы можете разделить изображение на его слои, используя . split() , и объединить отдельные полосы обратно в объект изображения, используя merge() . Когда вы используете . split() , метод возвращает все полосы как отдельные объекты изображения. Вы можете проверить это, отобразив строковое представление одного из возвращенных объектов:
>>> red, green, blue = img.split() >>> red >>> red.mode 'L'Режим объекта, который возвращает . split() , — «L», что указывает на то, что это изображение в градациях серого или же изображение отображает только значения яркости каждого пикселя.
Теперь вы можете создать три новых изображения RGB, отображенных к красном, зеленом и синем диапазоне, используя функцию merge(), которая является функцией модуля Image :
>>> zeroed_band = red.point(lambda _: 0) >>> red_merge = Image.merge( . "RGB", (red, zeroed_band, zeroed_band) . ) >>> green_merge = Image.merge( . "RGB", (zeroed_band, green, zeroed_band) . ) >>> blue_merge = Image.merge( . "RGB", (zeroed_band, zeroed_band, blue) . ) >>> red_merge.show() >>> green_merge.show() >>> blue_merge.show()Первый аргумент в функции merge() определяет слой изображения, которое вы хотите создать. Второй аргумент содержит отдельные слои, которые вы хотите объединить в одно изображение.
Один красный режим, сохраненный в переменной red, представляет собой изображение в градациях серого с режимом L. Чтобы создать изображение, показывающее только красный слой, вы объединяете красный диапазон из исходного изображения с зеленым и синем режимами, которые содержат только нули. Чтобы создать слой(режим), содержащий повсюду нули, вы используете метод . point() .
Этому методу нужна функция в качестве аргумента. Используемая функция определяет, как преобразуется каждая точка. В данном случае вы используете лямбда-функцию, чтобы сопоставить каждую точку с 0.
Когда вы объединяете красный режим с зеленым и синим, содержащими нули, вы получаете изображение RGB с именем red_merge . Таким образом, созданное вами изображение RGB имеет ненулевые значения только в красном режиме, но, поскольку это все еще изображение RGB, оно будет отображаться в цвете.
Вы также повторяете аналогичный процесс, чтобы получить green_merge и blue_merge , которые содержат изображения RGB с зеленым и синим режимами из исходного изображения. Код отображает следующие три изображения:

Красное изображение содержит сильный сигнал в пикселях, представляющих клубнику, потому что эти пиксели в основном красные. Зеленый и синий каналы показывают эти пиксели как темные, потому что они имеют малые значения. Исключением являются те пиксели, которые представляют собой отражение света на поверхности клубники, поскольку эти пиксели почти белые.
Создание общего изображения из нескольких
Когда требуется вывести несколько изображений рядом друг с другом, сохраненных одним файлом, можно воспользоваться следующей функцией:
from PIL import Image def tile(*images, vertical=False): width, height = images[0].width, images[0].height tiled_size = ( (width, height * len(images)) if vertical else (width * len(images), height) ) tiled_img = Image.new(images[0].mode, tiled_size) row, col = 0, 0 for image in images: tiled_img.paste(image, (row, col)) if vertical: col += height else: row += width return tiled_imgПервый параметр в tile() использует оператор распаковки (*), поэтому в качестве входных аргументов можно использовать любое количество объектов типа PIL.Image. Для параметра ключевого слова vertical можно установить значение True , если вы хотите размещать изображения вертикально, а не горизонтально. Эта функция предполагает, что все изображения имеют одинаковый размер.
Общий размер окончательного изображения рассчитывается исходя из размера изображений и количества используемых изображений. Затем вы создаете новый объект изображения с тем же режимом, что и исходные изображения, и с размером общего экрана.
Цикл for вставляет изображения, которые вы вводите при вызове функции, в окончательное изображение. Функция возвращает окончательный объект Image, содержащий все изображения рядом.
Изображение в данной статье, показывающее три цветовых режима для клубники, было получено путем вызова функции tile() следующим образом:
strawberry_channels = tile(red_merge, green_merge, blue_merge)Эта функция будет использоваться для создания всех изображений, которые состоят из нескольких в текущем руководстве.
Обработка изображений с помощью Pillow в Python
Вы узнали, как обрезать и поворачивать изображения, изменять их размер и изменять градации цвета изображений. Однако ни одно из действий, которые вы предприняли до сих пор, не внесло никаких изменений в содержимое изображения. В этом разделе вы узнаете о функциях обработки изображений в библиотеке Python Pillow. Вы будете использовать модуль ImageFilter в Pillow.
Фильтры изображений с использованием Convolution Kernels (ядра свертки)
Одним из методов, используемых при обработке изображений, является свертка изображений с использованием ядер (Convolution Kernels). Цель этого урока не в том, чтобы дать подробное объяснение теории обработки изображений. Если вас интересует наука об обработке изображений, одним из лучших ресурсов, который вы можете использовать, является Digital Image Processing by Gonzalez and Woods.
В этом разделе вы узнаете основы того, как можно использовать ядра свертки для обработки изображений. Но что такое ядро свертки? Ядро представляет собой матрицу:

Вы можете рассмотреть простое изображение, чтобы понять процесс свертки с использованием ядер. Изображение имеет размер 30×30 пикселей и содержит вертикальную линию и точку. Линия имеет ширину четыре пикселя, а точка состоит из квадрата 4×4 пикселя. Изображение ниже увеличено для наглядности:

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

Элементы на этой диаграмме представляют различные аспекты образа и ядра:
- Белые квадраты представляют собой пиксели изображения со значением 0.
- Красные квадраты представляют собой пиксели изображения со значением 255. Они составляют точку на изображении, показанном выше.
- Каждая фиолетовая область представляет собой ядро. Это ядро состоит из области 3×3, и каждая ячейка в ядре имеет значение 1/9. На диаграмме показано ядро в трех разных положениях, обозначенных цифрами 1, 2 и 3.
Новое изображение может быть создано в результате свертки изображения с ядром. Вы можете понять процесс свертки, выполнив следующие шаги:
- Найдите ядро: рассмотрите одно из местоположений ядра и посмотрите на пиксели изображения, покрытые девятью ячейками ядра.
- Умножение значений ядра и пикселей: умножьте значения в каждой из ячеек ядра на соответствующие значения пикселей в изображении. У вас будет девять значений из девяти операций.
- Суммируйте результаты умножения: сложите эти девять значений вместе. Результатом будет значение пикселя в новом изображении, которое имеет те же координаты, что и центральный пиксель ядра.
- Повторите для всех пикселей: повторите процесс для каждого пикселя изображения, каждый раз перемещая ядро так, чтобы центральная ячейка ядра каждый раз соответствовала другому пикселю изображения.
Вы можете увидеть этот процесс с тремя позициями ядра, помеченными 1, 2 и 3 на диаграмме выше. Рассмотрим положение ядра, обозначенное 1. Положение этого ядра равно (3, 2), что является положением его центральной ячейки, поскольку оно находится в четвертой строке (индекс = 3) и третьем столбце (индекс = 2). Каждый пиксель изображения в области, покрываемой ядром, имеет нулевое значение.
Следовательно, все умножения из шага 2 будут равны нулю, и их сложение тоже будет равно нулю. Новое изображение будет иметь нулевое значение в пикселе (3, 2).
Сценарий отличается для других показанных позиций ядра. Далее рассмотрим ядро с номером 2, расположенное в точках (4, 7). Один из пикселей изображения, не равен нулю. Умножение этого значения пикселя на значение ядра даст 255 x (1/9) = 28,33. Восемь оставшихся умножений по-прежнему равны нулю, поскольку пиксели изображения равны нулю. Следовательно, значение пикселя в позиции (4, 7) на новом изображении будет 28,33.
Третья позиция ядра, показанная выше, находится в (8, 11). С этим ядром перекрываются четыре ненулевых пикселя изображения. Каждый из них имеет значение 255, поэтому результат умножения снова будет 28,33 для каждой из этих позиций пикселей. Общий результат для этой позиции ядра равен 28,33 x 4 = 113,33. Новое изображение будет иметь значение (8, 11).
В данном случае мы рассмотрели только три положения ядра. Процесс свертки повторяет этот процесс для каждой возможной позиции ядра в изображении. Это дает значение для каждой позиции пикселя в новом изображении. Результат свертки показан справа на следующем изображении, а исходное изображение слева:

Ядро, которое вы использовали, представляет собой ядро размытия поля. Коэффициент 1/9 присутствует, так что общий вес ядра и равен 1. Результатом свертки является размытая версия исходного изображения. Существуют и другие ядра, выполняющие другие функции, в том числе различные методы размытия, определение границ, повышение резкости и многие другие.
Библиотека Python Pillow имеет несколько встроенных ядер и функций, которые будут выполнять свертки, описанные выше. Вам не нужно понимать математику фильтрации с помощью свертки, чтобы использовать эти фильтры, но всегда полезно знать, что происходит за кадром при использовании этих инструментов. В следующих разделах будут рассмотрены ядра и возможности фильтрации изображений, доступные в модуле ImageFilter в Pillow.
Размытие изображения, повышение резкости и сглаживание
Вернемся к изображению зданий, которое использовали в начале урока. Можно начать новую сессию REPL для этого раздела:
from PIL import Image, ImageFilter filename = "buildings.jpg" with Image.open(filename) as img: img.load()В дополнение к модулю Image вы также импортируете модуль ImageFilter из Pillow. Вы можете использовать метод . filter() для применения фильтрации к изображению. Этот метод требует ядра свертки в качестве аргумента, и вы можете использовать одно из нескольких ядер, доступных в модуле ImageFilter в Pillow. Первый набор фильтров, о котором вы узнаете, касается размытия, повышения резкости и сглаживания изображения.
Вы можете размыть изображение с помощью фильтра ImageFilter.BLUR:
blur_img = img.filter(ImageFilter.BLUR) blur_img.show()Отображаемое изображение является размытой версией исходного изображения. Вы можете увеличить масштаб, чтобы увидеть разницу более подробно, используя .crop() , а затем снова отобразить изображения, используя . show() :

Вы можете настроить необходимый тип и степень размытия, используя ImageFilter.BoxBlur() или ImageFilter.GaussianBlur() :
img.filter(ImageFilter.BoxBlur(5)).show() img.filter(ImageFilter.BoxBlur(20)).show() img.filter(ImageFilter.GaussianBlur(20)).show()Теперь имеется три размытых изображения, показанные в том же порядке, что и в приведенном выше коде:

Фильтр .BoxBlur() аналогичен фильтру, описанному в предыдущем разделе, где представлены ядра свертки. Аргументом является радиус фильтра размытия прямоугольника. В предыдущем разделе, посвященном ядрам, использовался фильтр рамочного размытия 3×3. Это означает, что он имел радиус 1, потому что фильтр простирается на один пиксель от центра.
Размытые изображения показывают, что прямоугольный фильтр размытия с радиусом 20 создает более размытое изображение, чем изображение, сгенерированное прямоугольным фильтром размытия с радиусом 5.
Вы также можете использовать фильтр . GaussianBlur() , который использует ядро размытия по Гауссу. Ядро Гаусса придает больший вес пикселям в центре ядра, чем по краям, и это приводит к более плавному размытию, чем то, что получается с размытием прямоугольника. По этой причине во многих случаях размытие по Гауссу может дать лучшие результаты.
Что делать, если вы хотите повысить резкость изображения? В этом случае вы можете использовать фильтр ImageFilter.SHARPEN и сравнить результат с исходным изображением:
sharp_img = img.filter(ImageFilter.SHARPEN) img.crop((300, 300, 500, 500)).show() sharp_img.crop((300, 300, 500, 500)).show()Вы можете сравнить обрезанную версию обоих изображений, показывающих небольшую часть здания.

Возможно, вместо повышения резкости, изображение нужно сгладить. Вы можете добиться этого, передав ImageFilter.SMOOTH в качестве аргумента для .filter():
smooth_img = img.filter(ImageFilter.SMOOTH) img.crop((300, 300, 500, 500)).show() smooth_img.crop((300, 300, 500, 500)).show()Ниже вы можете увидеть исходное изображение слева и сглаженное изображение справа:

Разберем применение сглаженного фильтра в следующем разделе, в котором вы узнаете о дополнительных фильтрах в модуле ImageFilter. Эти фильтры воздействуют на края объектов на изображении.
Обнаружение границ, приданием им четкости и тиснение
Когда вы смотрите на изображение, легко определить границы объектов на этом изображении. Алгоритм также может автоматически обнаруживать края, вызывая выявление границ.
Модуль ImageFilter в Pillow имеет предопределенное ядро для достижения этой цели. В этом разделе вы снова используете изображения зданий и конвертируете его в оттенки серого, прежде чем применять фильтр обнаружения границ. Вы можете продолжить сеанс REPL из прошлого раздела:
img_gray = img.convert("L") edges = img_gray.filter(ImageFilter.FIND_EDGES) edges.show()
Данный фильтр идентифицирует границы изображения. Вы можете получить лучший результат, применив фильтр ImageFilter.SMOOTH перед поиском границ:
img_gray_smooth = img_gray.filter(ImageFilter.SMOOTH) edges_smooth = img_gray_smooth.filter(ImageFilter.FIND_EDGES) edges_smooth.show()Изучите исходное изображение в градациях серого и два результата обнаружения границ ниже. Версия со сглаживанием перед обнаружением краев показана в самом низу:

Вы также можете улучшить границы исходного изображения с помощью фильтра ImageFilter.EDGE_ENHANCE :
edge_enhance = img_gray_smooth.filter(ImageFilter.EDGE_ENHANCE) edge_enhance.show()В данном случае используется сглаженная версия изображения в градациях серого для очерчивания границ. Часть исходного изображения в градациях серого и изображение с очерченными границами показаны рядом ниже.

Еще одним предопределенным фильтром в ImageFilter , который работает с границами объекта, является ImageFilter.EMBOSS . Вы можете передать его в качестве аргумента .filter(), как мы делали с другими фильтрами в этом разделе:
emboss = img_gray_smooth.filter(ImageFilter.EMBOSS) emboss.show()Вы используете сглаженную версию в градациях серого в качестве отправной точки для этого фильтра. Ниже представлено рельефное изображение, на котором показан другой эффект с обработкой границ на изображении:

В этом разделе вы узнали о нескольких доступных в модуле ImageFilter фильтрах, которые можно применять к изображениям. Существуют и другие фильтры. Вы можете увидеть список всех доступных фильтров в документации ImageFilter.
Сегментация и наложение изображений. Пример:
В этом разделе вы будете использовать файлы изображений с именами cat.jpg и monastery.jpg, которые вы можете найти в репозитории изображений для этого руководства. Вот два изображения:


Вы можете использовать библиотеку Python Pillow, чтобы извлечь кота из первого изображения и разместить его на полу монастырского двора. Для этого потребуется ряд методов обработки изображений.
Порог изображения
Начнем работу с cat.jpg. Сначала нужно будет удалить изображение кошки с фона, используя методы сегментации изображения. В этом примере сегментируется изображение, используя методы пороговой обработки. Сначала обрезаем изображение до меньшего размера, чтобы удалить часть фона. Для данного проекта можно начать новую сессию REPL для этого проекта:
from PIL import Image filename_cat = "cat.jpg" with Image.open(filename_cat) as img_cat: img_cat.load() img_cat = img_cat.crop((800, 0, 1650, 1281)) img_cat.show()Обрезанное изображение содержит кошку и часть фона, который максимально близок к кошке:

Каждый пиксель цветного изображения представляется в цифровом виде тремя числами, соответствующими значениям красного, зеленого и синего цвета этого пикселя. Поиск порогового значения — это процесс преобразования всех пикселей в максимальное или минимальное значение в зависимости от того, выше или ниже они определенного числа. Это проще сделать с изображением в градациях серого:
img_cat_gray = img_cat.convert("L") img_cat_gray.show() threshold = 100 img_cat_threshold = img_cat_gray.point( lambda x: 255 if x > threshold else 0 ) img_cat_threshold.show()Вы достигаете порога, вызывая . point() для преобразования каждого пикселя в изображении в градациях серого либо в 255, либо в 0. Преобразование зависит от того, больше или меньше значение в изображении в градациях серого, чем пороговое значение. Пороговое значение в этом примере равно 100.

В этом примере все точки изображения в градациях серого со значением пикселя больше 100 преобразуются в белые, а все остальные пиксели изменяются на черные. Вы можете изменить чувствительность процесса пороговой обработки, изменив пороговое значение.
Пороговое значение можно использовать для сегментации изображений, когда объект для сегментации отличается от фона. Вы можете добиться лучших результатов с более контрастными версиями исходного изображения. В этом примере вы можете добиться более высокой контрастности, установив порог синего канала исходного изображения, а не изображения в градациях серого, поскольку доминирующими цветами фона являются коричневый и зеленый цвета, которые имеют слабую синюю составляющую. Вы можете извлечь красный, зеленый и синий каналы из цветного изображения, как мы это делали ранее:
red, green, blue = img_cat.split() red.show() green.show() blue.show()
Синий режим имеет более высокий контраст между пикселями, представляющими кошку, и пикселями, представляющими фон. Вы можете использовать изображение синего режима для порога:
threshold = 57 img_cat_threshold = blue.point(lambda x: 255 if x > threshold else 0) img_cat_threshold = img_cat_threshold.convert("1") img_cat_threshold.show()В этом примере вы используете пороговое значение 57. Вы также конвертируете изображение в двоичный режим, используя «1» в качестве аргумента для .convert(). Пиксели в бинарном изображении могут иметь только значения 0 или 1.
Примечание. При работе с определенными форматами изображений, такими как JPEG, которые основаны на сжатии с потерями, изображения могут немного отличаться в зависимости от того, какие декодеры JPEG вы используете. Различные операционные системы часто поставляются со своими декодерами JPEG по умолчанию. Поэтому результаты, которые вы получаете при обработке изображений, могут различаться в зависимости от используемой операционной системы и декодера JPEG.
Возможно, вам придется немного скорректировать пороговое значение, если ваши результаты не совпадают с показанными в этом руководстве.

Вы можете узнать кошку на этом черно-белом изображении. Однако, наша цель — иметь изображение, в котором все пиксели, соответствующие кошке, белые, а все остальные пиксели — черные. На данном изображении все еще есть черные области в области, соответствующей кошке, например, где находятся глаза, нос и рот, а также есть белые пиксели в других местах изображения.
Вы можете использовать методы обработки изображения, называемые размытием и расширением, чтобы создать лучший образ, представляющий кошку. Вы узнаете об этих двух методах в следующем разделе.
Сжатие(erosion) и расширение(dilation)
Вы загрузить файл изображения с именем dot_and_hole.jpg из репозитория, представленного в первых главах туториала.

В левой части этого бинарного изображения показана белая точка на черном фоне, а в правой части — черная дыра на сплошном белом участке.
Сжатие(erosion) — это процесс удаления белых пикселей с границ изображения. Вы можете добиться этого в двоичном изображении, используя ImageFilter.MinFilter(3) в качестве аргумента для метода .filter() . Этот фильтр заменяет значение пикселя минимальным значением девяти пикселей в массиве 3×3, центрированном вокруг пикселя. В бинарном изображении это означает, что пиксель будет иметь нулевое значение, если любой из его соседних пикселей равен нулю. Вы можете увидеть эффект эрозии, применив ImageFilter.MinFilter(3) несколько раз к изображению dot_and_hole.jpg. Вам следует продолжить тот же сеанс REPL, что и в предыдущем разделе:
from PIL import ImageFilter filename = "dot_and_hole.jpg" with Image.open(filename) as img: img.load() for _ in range(3): img = img.filter(ImageFilter.MinFilter(3)) img.show()Мы применяем фильтр три раза, используя цикл for. Этот код дает следующий результат:

Точка уменьшилась, но отверстие увеличилось в результате эрозии. Дилатация – процесс, противоположный эрозии. Белые пиксели добавляются к границам бинарного изображения. Вы можете добиться расширения с помощью ImageFilter.MaxFilter(3), который преобразует пиксель в белый, если любой из его соседей белый. Вы можете применить расширение к тому же изображению, содержащему точку и отверстие, которое вы можете открыть и снова загрузить:
with Image.open(filename) as img: img.load() for _ in range(3): img = img.filter(ImageFilter.MaxFilter(3)) img.show()
Вы можете использовать эрозию и расширение вместе, чтобы заполнить дыры и удалить мелкие объекты из бинарного изображения. Используя изображение с точкой и отверстием, вы можете выполнить десять циклов эрозии, чтобы удалить точку, а затем десять циклов расширения, чтобы восстановить исходный размер отверстия:
with Image.open(filename) as img: img.load() for _ in range(10): img = img.filter(ImageFilter.MinFilter(3)) img.show() for _ in range(10): img = img.filter(ImageFilter.MaxFilter(3)) img.show()Вы выполняете десять циклов эрозии с помощью первого цикла for. Изображение на этом этапе выглядит следующим образом:

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

Однако точки больше нет на изображении. Сжатие и расширение изменили изображение, чтобы сохранить отверстие, но удалить точку. Количество необходимых сжатий и расширений зависит от изображения и от того, чего вы хотите достичь. Часто вам нужно будет найти правильную комбинацию методом проб и ошибок. Вы можете определить функции для выполнения нескольких циклов сжатия и расширения:
def erode(cycles, image): for _ in range(cycles): image = image.filter(ImageFilter.MinFilter(3)) return image def dilate(cycles, image): for _ in range(cycles): image = image.filter(ImageFilter.MaxFilter(3)) return imageЭти функции упрощают эксперименты с сжатием и расширением изображения. Вы будете использовать эти функции в следующем разделе, продолжая работать над помещением кота в монастырь.
Сегментация изображения с использованием порогового значения
Вы можете использовать последовательность сжатий и расширений на пороговом изображении, которое вы получили ранее, чтобы удалить части образа, которые не представляют кошку, и заполнить любые пробелы в области, содержащей кошку. После того, как вы поэкспериментировали со сжатием и расширением, вы сможете использовать обоснованные предположения в процессе проб и ошибок, чтобы найти наилучшее сочетание сжатий и расширений для получения идеального образа.
Начиная с изображения img_cat_threshold, которое вы получили ранее, вы можете начать с серии сжатий, чтобы удалить белые пиксели, представляющие фон исходного изображения. Следует продолжить работу в том же сеансе REPL, что и в предыдущих разделах:
step_1 = erode(12, img_cat_threshold) step_1.show()Новое пороговое изображение больше не содержит белых пикселей на фоне изображения:

Однако оставшийся образ меньше, чем общий контур кота, и внутри нее есть отверстия и промежутки. Вы можете выполнить расширение, чтобы заполнить пробелы:
step_2 = dilate(58, step_1) step_2.show()Пятьдесят восемь циклов расширения заполнили все отверстия в образе, чтобы получить следующее изображение:

Однако этот образ слишком велик. Таким образом, вы можете закончить процесс серией сжатий:
cat_mask = erode(45, step_2) cat_mask.show()В результате получится образ, который можно использовать для сегментации изображения кота:

Вы можете избежать острых краев бинарного образа, размыв этот образ. Сначала вам нужно преобразовать его из двоичного изображения в изображение в градациях серого:
cat_mask = cat_mask.convert("L") cat_mask = cat_mask.filter(ImageFilter.BoxBlur(20)) cat_mask.show()
Образ теперь выглядит как кошка! Теперь вы готовы извлечь изображение кота из фона:
blank = img_cat.point(lambda _: 0) cat_segmented = Image.composite(img_cat, blank, cat_mask) cat_segmented.show()Сначала вы создаете пустой образ того же размера, что и img_cat. Вы создаете новый объект изображения из img_cat , используя .point() и устанавливаете все значения равными нулю. Затем вы используете функцию Composite() в PIL.Image для создания изображения, содержащую параметры img_cat , blank , cat_mask , чтобы определить, какие части каждого изображения используются. Композитное изображение показано ниже:

Вы сегментировали изображение кота и выделили его из фона.
Наложение изображений с использованием Image.paste()
Вы можете пойти еще дальше и вставить сегментированное изображение кота в изображение монастырского двора из репозитория изображений для этого урока:
filename_monastery = "monastery.jpg" with Image.open(filename_monastery) as img_monastery: img_monastery.load() img_monastery.paste( img_cat.resize((img_cat.width // 5, img_cat.height // 5)), (1300, 750), cat_mask.resize((cat_mask.width // 5, cat_mask.height // 5)), ) img_monastery.show()Вы использовали . paste() для вставки изображения в другое. Этот метод можно использовать с тремя аргументами:
- Первый аргумент — это изображение, которое вы хотите вставить. Вы изменяете размер изображения до одной пятой его размера, используя оператор целочисленного деления (//).
- Второй аргумент — это место на основном изображении, куда вы хотите вставить второе изображение. Кортеж включает в себя координаты в основном изображении, где вы хотите поместить верхний левый угол изображения, которое вы вставляете.
- Третий аргумент предоставляет образ(маску), которую вы хотите использовать, если не хотите вставлять все изображение.
Вы использовали образ, полученный в процессе пороговой обработки, эрозии и расширения, чтобы вставить кошку без фона. На выходе получается следующее изображение:

Вы отделили кошку от одного изображения и поместили ее на другое изображение, чтобы показать кошку, спокойно сидящую во дворе монастыря, а не в поле, где она сидела на исходном изображении.
Создание водяного знака
Ваша последняя задача в этом примере — добавить логотип Real Python в качестве водяного знака на изображение. Вы можете получить файл изображения с логотипом Real Python из репозитория. Вы должны продолжить работу в том же сеансе REPL:
logo = "realpython-logo.png" with Image.open(logo) as img_logo: img_logo.load() img_logo = Image.open(logo) img_logo.show()
Вы можете изменить изображение на оттенки серого и установить пороговое значение с помощью .point() , чтобы преобразовать его в черно-белое изображение. Вы также уменьшаете его размер и преобразуете в контурное изображение:
img_logo = img_logo.convert("L") threshold = 50 img_logo = img_logo.point(lambda x: 255 if x > threshold else 0) img_logo = img_logo.resize( (img_logo.width // 2, img_logo.height // 2) ) img_logo = img_logo.filter(ImageFilter.CONTOUR) img_logo.show()На выходе показан контур логотипа Real Python. Контур идеально подходит для использования в качестве водяного знака на нашем изображении:

Чтобы использовать это в качестве водяного знака, вам нужно поменять местами цвета, чтобы фон был черным, а контур, который вы хотите сохранить, был белым. Вы можете добиться этого, снова используя .point() :
img_logo = img_logo.point(lambda x: 0 if x == 255 else 255) img_logo.show()Вы преобразовали пиксели со значением 255 и присвоили им значение 0, преобразовав их из белых в черные пиксели. Вы оставляете оставшиеся пиксели белыми. Логотип с преобразованным контуром показан ниже:

Ваш последний шаг — наложить этот контур на изображение кота, сидящего во дворе монастыря. Вы можете снова использовать . paste() :
img_monastery.paste(img_logo, (480, 160), img_logo) img_monastery.show()Первый аргумент в .paste() указывает изображение, которое вы хотите вставить, а третий аргумент представляет образ(маску). В данном случае вы используете то же изображение в качестве маски, потому что это бинарное изображение. Второй аргумент предоставляет верхние левые координаты области, куда вы хотите вставить изображение. Итоговый вариант теперь содержит водяной знак Real Python:

Водяной знак имеет прямоугольный контур, что является результатом контурного фильтра, использованный ранее. Если вы хотите удалить этот контур, вы можете обрезать изображение с помощью .crop() . Это упражнение вы можете попробовать самостоятельно.
Операции с изображениями с помощью NumPy и Pillow
Pillow имеет обширный выбор встроенных функций и фильтров. Однако бывают случаи, когда нужно пойти дальше и оперировать изображениями за пределами функций, которые уже доступны в Pillow.
Вы можете дальше манипулировать изображением с помощью NumPy. NumPy — очень популярная библиотека Python для работы с числовыми массивами, и это идеальный инструмент для использования с Pillow. Узнать больше о NumPy можно в NumPy Tutorial: Your First Steps Into Data Science in Python.
Когда вы конвертируете изображение в массив NumPy, вы можете выполнять любые необходимые преобразования непосредственно с пикселями в массиве. После завершения обработки в NumPy вы можете преобразовать массив обратно в объект изображения с помощью Pillow. Необходимо установить NumPy для этого раздела:
python -m pip install numpyТеперь, когда вы установили NumPy, вы готовы использовать Pillow и NumPy, чтобы определить разницу между двумя изображениями.
Использование NumPy для вычитания изображений друг из друга (поиск разницы)
Посмотрите, сможете ли вы найти различия между следующими двумя изображениями:

Это не сложно! Однако вы решаете схитрить и написать программу на Python, которая решит головоломку за вас. Вы можете загрузить файлы изображений house_left.jpg и house_right.jpg из репозитория.
Ваш первый шаг — прочитать изображения с помощью Pillow и преобразовать их в массивы NumPy:
>>> import numpy as np >>> from PIL import Image >>> with Image.open("house_left.jpg") as left: . left.load() . >>> with Image.open("house_right.jpg") as right: . right.load() . >>> left_array = np.asarray(left) >>> right_array = np.asarray(right) >>> type(left_array) >>> type(right_array)Поскольку left_array и right_array являются объектами типа numpy.ndarray, вы можете оперировать ими, используя все инструменты, доступные в NumPy. Можно вычесть один массив из другого, чтобы показать пиксели, которые различаются между двумя изображениями:
>>> difference_array = right_array - left_array >>> type(difference_array)
Когда вы вычитаете массив из другого массива того же размера, в результате получается еще один массив той же формы, что и исходные массивы. Вы можете преобразовать этот массив в изображение, используя Image.fromarray() в Pillow:
difference = Image.fromarray(difference_array) difference.show()Результатом вычитания одного массива NumPy из другого и преобразования в Pilllow Image является разностное изображение, показанное ниже:

Разностное изображение показывает только три области исходного изображения. Эти области подчеркивают различия между двумя изображениями. Вы также можете увидеть некоторый шум вокруг облака и забора из-за небольших изменений в исходном сжатии JPEG в области, окружающей эти объекты.
Использование NumPy для создания изображений
Вы можете пойти дальше и создавать объкты с нуля, используя NumPy и Pillow. Вы можете начать с создания изображения в градациях серого. В этом примере вы создадите простое изображение, содержащее квадрат, но таким же образом вы можете создать более сложные изображения:
>>> import numpy as np >>> from PIL import Image >>> square = np.zeros((600, 600)) >>> square[200:400, 200:400] = 255 >>> square_img = Image.fromarray(square) >>> square_img >>> square_img.show()Вы создаете массив размером 600×600, содержащий везде нули. Затем устанавливаете значение набора пикселей в центре массива равным 255.
Вы можете индексировать массивы NumPy, используя как строки, так и столбцы. В этом примере первый срез, 200:400, представляет строки с 200 по 399. Второй срез, 200:400, следующий за запятой, представляет столбцы с 200 по 399.
Также можно использовать Image.fromarray() для преобразования массива NumPy в объект типа Image. Вывод кода выше показан ниже:

Вы создали изображение в градациях серого, содержащее квадрат. Режим изображения выводится автоматически, когда вы используете Image .fromarray() . В этом случае используется режим «F», который соответствует изображению с 32-битными пикселями с плавающей запятой. Вы можете преобразовать это в более простое изображение в градациях серого с 8-битными пикселями:
square_img = square_img.convert("L")Вы также можете пойти дальше и создать цветное изображение. Повторим описанный выше процесс, чтобы создать три изображения, одно из которых соответствует красному каналу, другое — зеленому, а последнее — синему каналу:
red = np.zeros((600, 600)) green = np.zeros((600, 600)) blue = np.zeros((600, 600)) red[150:350, 150:350] = 255 green[200:400, 200:400] = 255 blue[250:450, 250:450] = 255 red_img = Image.fromarray(red).convert("L") green_img = Image.fromarray(green).convert("L") blue_img = Image.fromarray((blue)).convert("L")Вы создаете объект Image из каждого массива NumPy и конвертируете изображения в режим «L», который представляет оттенки серого. Теперь вы можете объединить эти три отдельных изображения в одно изображение RGB, используя Image.merge() :
>>> square_img = Image.merge("RGB", (red_img, green_img, blue_img)) >>> square_img >>> square_img.show()Первый аргумент в Image.merge() — это режим вывода изображения. Второй аргумент — последовательность с отдельными изображениями. Этот код создает следующее изображение:

Вы объединили отдельные режимы в цветное изображение RGB. В следующем разделе вы сделаете еще один шаг и создадите GIF-анимацию с помощью NumPy и Pillow.
Создание анимации
В предыдущем разделе мы создали цветное изображение, содержащее три перекрывающихся квадрата разных цветов. В этом разделе вы создадите анимацию, показывающую, как эти три квадрата сливаются в один белый квадрат. Создадим несколько версий изображений, содержащих три квадрата, и расположение квадратов будет немного различаться между последовательными изображениями:
import numpy as np from PIL import Image square_animation = [] for offset in range(0, 100, 2): red = np.zeros((600, 600)) green = np.zeros((600, 600)) blue = np.zeros((600, 600)) red[101 + offset : 301 + offset, 101 + offset : 301 + offset] = 255 green[200:400, 200:400] = 255 blue[299 - offset : 499 - offset, 299 - offset : 499 - offset] = 255 red_img = Image.fromarray(red).convert("L") green_img = Image.fromarray(green).convert("L") blue_img = Image.fromarray((blue)).convert("L") square_animation.append( Image.merge( "RGB", (red_img, green_img, blue_img) ) )Вы создаете пустой список с именем Square_Animation , который будет использоваться для хранения различных изображений, которые вы создаете. В цикле for создаем массивы NumPy для красного, зеленого и синего слоев, как мы это делали в предыдущем разделе. Массив, содержащий зеленый слой, всегда один и тот же и представляет собой квадрат в центре изображения.
Красный квадрат начинается со смещения влево вверх от центра. В каждом последующем кадре красный квадрат перемещается ближе к центру, пока не достигнет центра в последней итерации цикла. Синий квадрат сначала смещается в правый нижний угол, а затем перемещается к центру с каждой итерацией.
Обратите внимание, что в этом примере вы перебираете диапазон (0, 100, 2), что означает, что смещение переменной увеличивается с шагом в два. Ранее вы узнали, что вы можете сохранить объект изображения в файл, используя Image.save() . Вы можете использовать ту же функцию для сохранения файла GIF, содержащего последовательность изображений. Вы вызываете Image.save() для первого изображения в последовательности, которое является первым изображением, которое вы сохранили в списке Square_Animation :
square_animation[0].save( "animation.gif", save_all=True, append_images=square_animation[1:] )Первый аргумент в .save() — это имя файла, который вы хотите сохранить. Расширение в имени файла сообщает .save() , какой формат файла необходимо вывести. Вы также включаете два аргумента ключевого слова в . save() :
- save_all=True гарантирует, что будут сохранены все изображения в последовательности, а не только первое.
- append_images=square_animation[1:] позволяет добавить оставшиеся изображения в последовательности в файл GIF.
Этот код сохраняет анимацию.gif в файл, после чего вы можете открыть файл GIF с помощью любого программного обеспечения для работы с изображениями. GIF должен зацикливаться по умолчанию, но в некоторых системах вам нужно будет добавить ключевой аргумент loop=0 в . save() , чтобы обеспечить зацикливание GIF. Анимация, которую вы получите, выглядит следующим образом:

Три квадрата разных цветов сливаются в один белый квадрат. Можете ли вы создать свою собственную анимацию, используя разные формы и разные цвета?
Заключение
Вы узнали, как использовать Pillow для работы с изображениями и выполнения обработки. Если вам понравилось работать с изображениями, вы можете с головой окунуться в мир дизайна. Еще многое предстоит узнать о теории и практики обработки изображений. Хорошей отправной точкой является Digital Image Processing by Gonzalez and Woods, который является классическим учебником в этой области.
Pillow — не единственная библиотека, которую используется в Python для обработки изображений. Если ваша цель состоит в том, чтобы выполнить некоторую базовую обработку, то методы, которые вы изучили в этом руководстве, могут быть всем, что вам нужно.
Если вы хотите углубиться в более продвинутые методы обработки изображений, например, для приложений машинного обучения и компьютерного зрения, вы можете использовать Pillow в качестве трамплина для других библиотек, таких как OpenCV и scikit-image.
В этом уроке вы узнали, как:
- Читать изображения с помощью Pillow
- Выполнять основные операции с изображениями
- Использовать Pilow для обработки изображений
- Используйте NumPy с Pillow для дальнейшей обработки
- Создавать анимации с помощью Pillow
Теперь просмотрите изображения на вашем компьютере и выберите несколько, которые вы можете прочитать как изображения с помощью Pillow. Решите, как вы хотите обработать эти изображения, а затем выполните некоторую обработку. Удачи!
Примечания
Метод img.transpose(Image.FLIP_TOP_BOTTOM) является устаревшим (приведен в первой главе). Вместо данной строки вводите img.transpose(Image.Transpose.FLIP_TOP_BOTTOM) .
Если img.show() не работает в вашей системе, то используйте img.save() , чтобы сохранить и просмотреть полученное изображение.
Ссылка на гитхаб, где я разбирала кадрирование, сжатие и склеивание изображений тут.


