Первого января 2015 года по расписанию вышла новая версия библиотеки для работы с изображениями Pillow 2.7. Так как многие изменения в ней были сделаны командой Uploadcare, мы рады представить вам расширенную версию заметок о релизе этой версии.
Для начала вспомним, с чего все началось. Pillow — дружественный форк (как называют его авторы) популярной библиотеки PIL, Python Imaging Library. Последняя версия PIL 1.1.7 вышла в 2009 году и в основном содержала исправления ошибок. Изначально Pillow задумывался как проект только по приведению в порядок сборки PIL, и разработчики рекомендовали отправлять все баги, не связанные со сборкой, в оригинальный PIL. Но время шло, PIL стремительно устаревала, багов не уменьшалось, тут еще Python 3 маячил на горизонте. Поэтому с версией Pillow 2.0 все изменилось. «Pillow 2.0.0 добавляет поддержку Python 3 и включает много багфиксов со всего интернета» гласит описание проекта на PyPI. И с тех пор понеслось. Каждые три месяца выходили версии с огромных количеством багфиксов и другими улучшениями от различных разработчиков. Самое значительное нововведение за это время было, пожалуй, поддержка форматов WebP и JPEG2000. Теперь пришло время следующего большого шага.
Фильтры ресайза изображений
Функции ресайза изображений Image.resize()
и Image.thumbnail()
в качестве одного из аргументов принимают resample
— фильтр, использующийся для ресайза. Его возможные значения: NEAREST
, BILINEAR
, BICUBIC
и ANTIALIAS
. Поведение практически каждого из них изменилось в новой версии.
Уменьшение изображения с билинейным и бикубическим фильтрами
Одной из проблем в PIL, а потом и в Pillow, было то, что для ресайза с помощью билинейного и бикубического фильтра использовался метод аффинных преобразований, который использует одно и то же количество пикселей исходного изображения для формирования одного пикселя конечного (2x2 пикселя для билинейного, 4x4 для бикубического) и фиксированный размер фильтра. Это приводило к неудовлетворительным результатам для уменьшения изображения, практически не отличавшимся от метода ближайшего соседа.
Слева метод ближайшего соседа, справа бикубический фильтр аффинных преобразований. Первый образец — уменьшение в 5,8 раз, различий практически нет. Второй — в 1,8 раз, отличия минимальные, на резких диагональных линиях видна лесенка.
В то же время для фильтра ANTIALIAS
использовался высококачественный алгоритм на основе сверток, что давало одинаково хороший результат как для уменьшения, так и для увеличения.
Начиная с Pillow 2.7.0, высококачественный алгоритм на основе сверток используется для всех трех фильтров.
Слева бикубический фильтр на основе аффинных преобразований, справа — свертки. Свертки определенно выигрывают.
Если до того вы использовали какие-то ухищрения для улучшения качества при использовании билинейного или бикубического фильтра (например, уменьшение изображения за несколько шагов или предварительное размытие), теперь в них нет необходимости.
Antialias переименован в Lanczos
Новая константа Image.LANCZOS
была добавлена взамен Image.ANTIALIAS
.
Когда метод ANTIALIAS
был впервые представлен, он был единственным высококачественным методом, основанным на свертках. И его имя отражало этот факт. Теперь, когда все методы основаны на свертках, они все стали «сглаживающими». А настоящее название фильтра, которое использовалось раньше для этой константы — фильтр Ланцоша.
Само собой, старая константа оставлена для обратной совместимости и является псевдонимом для новой. Шутка для лингвистов: Antialias is alias now.
Качество фильтра Ланцоша при увеличении
Как ни странно, с качеством сверок тоже было не все в порядке. В предыдущих версиях был баг, из-за которого качество фильтра Ланцоша при увеличении было практически таким же, как у фильтра BILINEAR
. Этот баг был исправлен.
Слева результат увеличения в 4,3 раза предыдущей версии, справа — Pillow 2.7.0. Картинки слева одновременно более размытые и пикселизованные.
Качество бикубического фильтра при увеличении
Бикубический фильтр, реализованный для аффинных преобразований, давал резкую, слегка пикселизованную картинку при увеличении. Бикубический фильтр, реализованный для сверток, немного мягче.
Слева результат увеличения в 4,3 раза предыдущей версии, справа — Pillow 2.7.0. Картинки слева более пикселизованные (имеют более ощутимые границы пикселей). В то же время диагональные линии на первом примере более четкие и менее подвержены эффекту лесенки. И то и другое — влияние параметра «a» в бикубическом уравнении. Избежать обоих эффектов можно только с помощью более качественного фильтра Ланцоша.
Производительность ресайза
В общем случае свертки — более затратный алгоритм для уменьшения, потому что в отличие от аффинных преобразований, он учитывает все пиксели исходного изображения. Из-за этого чистая производительность билинейного и бикубического фильтров может быть ниже, чем раньше. С другой стороны, если вы до этого были удовлетворены качеством билинейного и бикубического фильтров для уменьшения, возможно вам стоит подумать над использованием NEAREST
фильтра, который давал практически такой же результат. Это существенно увеличит производительность.
В то же время одно из существенных улучшений Pillow 2.7.0 в том, что производительность сверток для уменьшения была увеличена в среднем в 2 раза по сравнению с предыдущей версией и даже по сравнению с ImageMagick. Производительность увеличения свертками для фильтра BILINEAR
оказалась быстрее в полтора раза, для BICUBIC
— в четыре, а для LANCZOS
осталась на том же уровне.
Т.к. скорее всего вы не использовали в своем приложении ничего, кроме LANCZOS
(бывший ANTIALIAS
), то производительность при уменьшении для вас должна увеличиться в среднем в два раза. Если использование Ланцоша для вас было вынужденной мерой из-за низкого качества остальных фильтров, то теперь вы можете перейти, например, на билинейный фильтр. Это увеличит производительность еще примерно в 2 раза для уменьшения и примерно на 30% для увеличения.
Фильтр по умолчанию для Image.thumbnail()
В Pillow 2.5 фильтр по умолчанию для Image.thumbnail()
был изменен с NEAREST
на ANTIALIAS
. Этот фильтр был выбран по причине, неоднократно озвученной выше — низкое качество остальных фильтров. В Pillow 2.7.0 фильтр по умолчанию вновь изменен, в этот раз на BICUBIC
, потому что он немного быстрее. На самом деле Ланцош не дает каких-либо преимуществ после использования метода Image.draft()
внутри Image.thumbnail()
, который уменьшает изображение с помощью библиотеки libjpeg
и использует для этого суперсемплинг, а не свертки.
Транспонирование изображений
Новый метод Image.TRANSPOSE
был добавлен для функции Image.transpose()
в дополнение к уже существующим FLIP_LEFT_RIGHT
, FLIP_TOP_BOTTOM
, ROTATE_90
, ROTATE_180
, ROTATE_270
. TRANSPOSE
— это алгебраическое транспонирование, т.е. отражение изображения относительно его основной диагонали.
Производительность методов ROTATE_90
, ROTATE_270
и TRANSPOSE
была существенно увеличена для больших изображений, не помещающихся в кэш процессора.
Эти три метода объединяет то, что в них пиксели берутся из строк, а помещаются в столбцы. Такой шаблон доступа к памяти оказывается очень не эффективным для больших изображений, потому что данные успевают вытесниться из кэша процессора за один проход и их приходится заново загружать из памяти для следующего прохода.
В новой версии изображение разбивается на логические квадраты размером в 128×128 пикселей, и операции над пикселями производятся последовательно внутри каждого квадрата. Это позволяет существенно сократить дистанцию, которую проходит процессор на каждой строке, в результате чего данные не успевают вытесниться из кэша (память, необходимая для одного квадрата, равна 64Кб).
Гауссово размытие и контурная резкость
Реализация ImageFilter.GaussianBlur
была заменена на последовательное применение бокс-фильтров. Новая реализация основана на статье Theoretical foundations of Gaussian convolution by extended box filtering от Mathematical Image Analysis Group. Так как реализация ImageFilter.UnsharpMask
базируется на Гауссовом размытии, все, что описано в этой секции, также применимо и к ней.
Радиус размытия
В предыдущих версиях Pillow была ошибка, из-за которой радиус размытия (стандартное отклонение Гауссианы) на самом деле задавал его диаметр. Поэтому, например, чтобы размыть изображение на радиус 5, нужно было указывать значение 10. Ошибка была исправлена, и теперь значение радиуса интерпретируется так же, как во всем остальном программном обеспечении.
Если до этого вы использовали Гауссово размытие с определенным радиусом, вам нужно поделить его значение на два.
Производительность размытия
Время вычисления бокс-фильтра постоянно относительно его радиуса и зависит только от размеров входного изображения. Т.к. новая реализация Гауссового размытия основана на бокс-фильтре, её вычисление также не зависит от радиуса размытия.
Для радиуса в 1 пиксель новая реализация работает 5 раз быстрее, для радиуса 10 — в 18 раз, для радиуса 50 — уже в 85 раз. Ваш дизайнер, рисующий интерфейсы в стиле iOS 8, должен быть доволен.
Качество размытия
Теоретически при Гауссовом размытии в вычислении каждой точки конечного изображения должны участвовать все точки исходного с определенными коэффициентами. На практике коэффициенты точек дальше 3×стандартное отклонение настолько малы, что учитывать их нет смысла.
Предыдущая реализация учитывала только пиксели в радиусе 2×стандартное отклонение для каждого конечного пикселя. Это было недостаточно, поэтому качество было хуже в сравнении с другими реализациями Гауссова размытия.
Несмотря на то, что новая реализация является лишь математической аппроксимацией, она не содержит такого бага.
Слева результат размытия с радиусом 5 в предыдущей версии (с учетом бага с удвоением радиуса), справа — в новой. Слева видны резкие границы объектов.
Все эти изменения уже работают на наших серверах. Благодаря им мы повысили качество и скорость API для обработки картинок на лету. Также мы реализовали операцию быстрого блюра. Но это еще не все. Мы готовим следующий большой шаг для Pillow, о котором объявим чуть позже.
Автор: homm