habrahabr

Pillow 2.7 — Существенное улучшение качества и производительности

  • суббота, 3 января 2015 г. в 02:10:50
http://habrahabr.ru/post/247219/

Первого января 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 для бикубического) и фиксированный размер фильтра. Это приводило к неудовлетворительным результатам для уменьшения изображения, практически не отличавшимся от метода ближайшего соседа.

nearestaffine
nearestaffine

Слева метод ближайшего соседа, справа бикубический фильтр аффинных преобразований. Первый образец — уменьшение в 5,8 раз, различий практически нет. Второй — в 1,8 раз, отличия минимальные, на резких диагональных линиях видна лесенка.

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

Начиная с Pillow 2.7.0, высококачественный алгоритм на основе сверток используется для всех трех фильтров.

affineconvolution
affineconvolution

Слева бикубический фильтр на основе аффинных преобразований, справа — свертки. Свертки определенно выигрывают.

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

Antialias переименован в Lanczos


Новая константа Image.LANCZOS была добавлена взамен Image.ANTIALIAS.

Когда метод ANTIALIAS был впервые представлен, он был единственным высококачественным методом, основанным на свертках. И его имя отражало этот факт. Теперь, когда все методы основаны на свертках, они все стали «сглаживающими». А настоящее название фильтра, которое использовалось раньше для этой константы — фильтр Ланцоша.

Само собой, старая константа оставлена для обратной совместимости и является псевдонимом для новой. Шутка для лингвистов: Antialias is alias now.

Качество фильтра Ланцоша при увеличении


Как ни странно, с качеством сверок тоже было не все в порядке. В предыдущих версиях был баг, из-за которого качество фильтра Ланцоша при увеличении было практически таким же, как у фильтра BILINEAR. Этот баг был исправлен.

beforeafter
beforeafter

Слева результат увеличения в 4,3 раза предыдущей версии, справа — Pillow 2.7.0. Картинки слева одновременно более размытые и пикселизованные.

Качество бикубического фильтра при увеличении


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

beforeafter
beforeafter

Слева результат увеличения в 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×стандартное отклонение для каждого конечного пикселя. Это было недостаточно, поэтому качество было хуже в сравнении с другими реализациями Гауссова размытия.

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

beforeafter
beforeafter

Слева результат размытия с радиусом 5 в предыдущей версии (с учетом бага с удвоением радиуса), справа — в новой. Слева видны резкие границы объектов.



Все эти изменения уже работают на наших серверах. Благодаря им мы повысили качество и скорость API для обработки картинок на лету. Также мы реализовали операцию быстрого блюра. Но это еще не все. Мы готовим следующий большой шаг для Pillow, о котором объявим чуть позже.