Эффект Blur в iOS приложениях

This post is also available in: Английский

Внешний вид является одной из важнейших составляющих успеха приложения на iOS. Стильный продуманный дизайн и эффектный внешний вид всегда производят на пользователя исключительно положительное впечатление и добавляют очков в пользу вашего приложения при выборе его в App Store.

Как произвести приятное впечатление на пользователя? Хорошего внешнего вида можно добиться множеством способов, одним из которых является наложение эффектов на различные части интерфейса и изображение в целом. О, пожалуй, самом распространенном из таких эффектов — эффекте Blur — и пойдет речь в данной статье. Мы рассмотрим способы наложения фильтра Blur с использованием различных фреймворков, а также проведем небольшой эксперимент и сравним производительность рассмотренных способов.

Что такое Blur?

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

В жизни вы, скорее всего, часто встречались с эффектом Blur — это эффект размытия картинки, который можно наблюдать на фотографиях, если объекты движутся относительно камеры или находятся не в фокусе. В кино и играх этот эффект часто используют для того, чтобы подчеркнуть динамичность снимаемой сцены или выделить в кадре важные детали, размыв изображения всех остальных. Используя эффект Blur в приложениях, вы можете придать реалистичности вашему интерфейсу, слегка размыв изображение движущихся элементов относительно неподвижных.

Для того, чтобы наложить Blur на изображение, в компьютерных приложениях используется операция над картинкой, называемая «сверткой» (convolution). Свертка — это получение нового изображения путем применения к каждому пикселю исходного изображения взвешенной маски, учитывающей яркости соседних пикселей. Если мы говорим о применении эффекта Blur в приложениях на iOS, то часто нам незачем знать механизм наложения во всех деталях. В большинстве случаев, для наложения этого эффекта будет предоставлен готовый объект фильтра, который нужно будет просто «применить» к картинке. Однако некоторые низкоуровневые фреймворки наподобие vImage не имеют подобных высокоуровневых возможностей и, работая близко к железу, оперируют именно такими понятиями, как kernel (взвешенная маска) и свертка. Поэтому, я думаю, вам будет далеко не лишним узнать о механизме размытия картинки на самом общем уровне.

Blur в Core Image

Итак, какими же инструментами для обработки изображений мы располагаем в iOS? Конечно первое, что приходит на ум, когда возникает желание каким-либо образом обработать изображение, — это фреймворк Core Image.

Core Image — это фреймворк, созданный специально для обработки и анализа статичных изображений и видео. Core Image стал доступен разработчикам, начиная с версии iOS 5. В своей работе он может использовать не только ресурсы CPU, но и GPU. Core Image изменяет изображения с помощью фильтров. Фильтр — это класс, предназначенный для абстрагирования конкретного визуального эффекта. Чтобы добавить эффект к картинке, нужно направить ее на вход фильтра, пропустить ее через фильтр, — и получить результирующее изображение на выходе фильтра. Как выглядит процесс добавления визуального эффекта к изображению на практике? Все предельно просто и умещается в четыре простых этапа.

  1. Создаем объект изображения CIImage и контекста CIContext.
  2. Получаем нужный фильтр из списка фильтров, существующих для данной системы во фреймворке Core Image.
  3. Связываем фильтр с картинкой.
  4. Получаем новую картинку на выходе фильтра.

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

В общем-то это все, что нужно сделать для получения эффекта размытия на картинке с помощью фреймворка Core Image. Мы просто находим нужный blur-фильтр и применяем его к картинке. Нужно отметить, что не все фильтры, перечисленные в документации, доступны на iOS. По умолчанию все фильтры доступны на MacOS. Дополнительно в описании каждого фильтра специально отмечено, возможно ли его использование в iOS. Фреймворк постоянно развивается, пополняясь новыми фильтрами, поэтому обязательно обращайте внимание на версию указанной iOS, т.к. некоторые из фильтров были добавлены только в iOS 6.

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

Совет номер один. Если ваше приложение предполагает частое наложение фильтров, не создавайте заново контекст CIContext при каждом вызове метода преобразования картинки. Это равносильно созданию контекста OpenGL для каждого нового кадра изображения. Создайте контекст один раз при инициализации объекта и используйте его в дальнейшем. Как показали исследования, повторное создание контекста — задача очень ресурсоемкая. Не надо перенагружать процессор.

Совет номер два. Разумно подходите к вопросу выбора процессора, обрабатывающего изображение: это может быть CPU, либо GPU. Использование GPU даст преимущество только в том случае, если вам необходимо обрабатывать видео с камеры в режиме текущего времени. Практически во всех остальных случаях использование GPU не дает желаемых преимуществ и имеет свои недостатки. Если в процессе обработки картинки на GPU вы выйдете из приложения, то вся информация об изображении, которая хранилась в тот момент в памяти графического процессора, будет утеряна. Обработка на центральном процессоре, наоборот, позволит вам вести обработку в фоновом режиме. Размер изображений для обработки на GPU ограничивается числом 4kx4k пикселей на устройстве iPad 2 и 2kx2k на более старых моделях, в то время как CPU поддерживает 8kx8k пикселей. Также нужно помнить, что если вы одновременно используете Core Animation и Core Graphics, то они могут конфликтовать из-за использования ресурсов GPU, что, опять-таки, будет тормозить работу приложения.

Для того, чтобы явно указать фреймворку Core Image, что обработку необходимо вести с использованием графического процессора, нужно создать объект CIContext, используя статический метод contextWithEAGLContext.

Пример получения картинки с эффектом Gaussian Blur приведен ниже.

[gist id=4493284 bump=1]

Пример предполагает, что контекст CIContext создан как свойство (property) внутри объекта.

На сайте raywenderlich.com есть хорошая вводная статья, рассказывающая об основах Core Image. Также в качестве примера можно рассмотреть приложение LBBlurredImage, использующее этот фреймворк.

Blur в vImage

Начиная с версии OS X v10.3, когда устройства Apple начали поддерживать процессоры Intel, в системный API введен фреймворк Accelerate, дополненный до современной версии в OS X v10.4. Его целью является обеспечение выполнения различных математических операций над векторами данных с использованием векторного сопроцессора. Среди преимуществ фреймворка обычно указывается его универсальность: в нем нет привязки к какому-то конкретному типу CPU, он умеет сам распознавать сопроцессор для выполнения векторных операций и, при его наличии, подключать его к математическим расчетам. Фреймворк спроектирован и создан таким образом, что разработчикам, которые его применяют, не нужно заботиться об обеспечении многопоточности выполняемых операций. Как описано в справке Apple по Accelerate, фреймворк сам распараллелит задачи по потокам, если такая возможность имеется, и сам выберет наиболее оптимальный способ выполнения с помощью сопроцессора векторных операций, при его наличии. Более подробное описание Accelerate дано на официальном сайте Apple для разработчиков, а также на MacResearch.

Accelerate состоит из двух фреймворков: vImage — для обработки изображений и vecLib — для выполнения различных математических операций, используемых при обработке цифровых сигналов. Само собой, в рамках данной статьи нас интересует vImage. Сравнивая vImage с Core Image, необходимо в первую очередь упомянуть, что их функционал частично перекрывается. При этом, если вам нужна только лишь обработка статичных картинок не слишком высокого разрешения, то Core Image вполне пригоден. Однако в том случае, если необходимых вам эффектов нет среди имеющихся в Core Image, или вы хотели бы их как-нибудь модифицировать, то фреймворк vImage может быть более предпочтительным, т.к. он позволяет более гибко и эффективно контролировать процесс преобразования пиксельной матрицы изображения. vImage также может стать более удачным решением в том случае, если вам необходимо быстро обрабатывать изображения высокого разрешения. Еще одним преимуществом vImage перед Core Image является то, что vImage поддерживается всеми системами, начиная с версии OS X 10.4.

API фреймворка vImage написан на языке С и представляет собой набор функций и типов данных. В нем нет объектов, классов и методов, выделять память в нем приходится с использованием malloc, а оперировать — исключительно наборами указателей и адресов. Любители объектно-ориентированной парадигмы будут недовольны, но подобная низкоуровневость — закономерная плата за быстродействие.

Итак, что же может предложить нам vImage для создания эффекта Blur? Этот фреймворк предоставляет вам полную свободу самостоятельно сконфигурировать маску и выполнить свертку, или же другие операции над пиксельной матрицей изображения.

Для выполнения свертки с произвольной маской, нужно вызвать одну из функций семейства

vImage_Error vImageConvolve_* (…),

где вместо * нужно поставить формат данных изображения. Фреймворк vImage использует один из нескольких форматов представления данных изображения; подробнее об этом можно узнать в официальной документации. В функцию должны быть переданы адреса буферов для входных и выходных данных, маска и другие параметры. Если что-то пойдет не так, вызов функции вернет номер ошибки, а в выходной буфер ничего не запишется. Стоит особо подчеркнуть, что маска представляет собой двухмерную матрицу, а количество элементов по ее измерениям в соответствии с документацией должно быть нечетным.

В дополнение к стандартным функциям свертки общего назначения с передачей маски, vImage предоставляет специальные функции наложения эффектов с готовой маской. Такие функции есть и для наложения эффекта размытия:

vImage_Error vImageBoxConvolve_* (…);
vImage_Error vImageTentConvolve_* (…);

Эти функции не требуют от разработчика передачи адреса маски. Они используют свою собственную стандартную маску для размытия. При этом скорость выполнения этих функций может быть на порядки выше скорости общей функции свертки, описанной выше. Более подробно о функциях можно прочитать в официальном руководстве.

Однако на применении свертки к изображению работа не заканчивается (мы предупреждали, что с этим фреймворком не все так просто)! Выполнив свертку и получив картинку с размытием, вы с удивлением обнаружите, что на картинке поменялись цвета. Что же случилось? Все дело в оптимизации, применяемой Apple для изображений внутри iOS. Подробно об этом можно почитать здесь, а мы лишь в общих чертах расскажем об этой неприятности. Для оптимизации работы с изображениями в картинках меняются местами каналы красного и синего цвета. То есть из формата RGBA изображение переходит в BGRA. Но нам повезло: vImage предлагает решение и для этой проблемы — при помощи функции vImagePermuteChannels_ARGB8888. Она позволяет задать маску в виде комбинации 4 цифр — 0, 1, 2 и 3, обозначающих, соответственно, каналы R, G, B и A. Расположение цифр в маске обозначает очередность каналов. Таким образом, для перехода обратно в стандартный цветовой формат необходимо вызвать эту функцию с маской 2103.

Но довольно рассказов! Давайте посмотрим на фреймворк в действии. Используйте этот метод для получения размытой картинки из переданной.

[gist id=4396237 bump=1]

Как видно из кода, vImage должен получить «сырые» данные изображения, выполнить свертку, а затем собрать их обратно в UIImage. На сайте IndieAmbitions.com есть прекрасное руководство по созданию размытого изображения с использованием vImage. Также в качестве примера можно использовать приложение RNBlurModalView, использующее фреймворк Accelerate для размытия интерфейса при появлении UIAlertView.

Blur в GPUImage

Весьма известный фреймворк для обработки изображений и видео — GPUImage — также предоставляет средства для наложения эффекта blur на изображение. Этот фреймворк, пожалуй, является рекордсменом по простоте выполнения размытия. Как и в случае Core Graphics, обработка изображений в нем происходит с помощью наложения фильтров. GPUImage максимально эффективно использует OpenGLES для обработки изображений, а значит вся работа происходит на GPU.

Процесс наложения фильтра здесь еще проще, чем в Core Image! Нужно только создать фильтр и отправить ему на вход изображение. Не нужно заботиться о контексте или о том, на каком устройстве происходит обработка. Эта часть кода инкапсулирована в код фреймворка.

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

Данный фреймворк включает в себя сразу несколько blur-фильтров. Строго говоря, в Core Image их еще больше (целых 7), однако из всего этого многообразия для использования в iOS на данный момент доступен только один — GaussianBlur. В GPUImage вам доступны фильтры: FastBlur, GaussianBlur, GaussianSelectiveBlur и BoxBlur.

Пример наложения эффекта размытия с помощью этого фреймворка предельно краток и прост.

[gist id=4493268 bump=1]

В качестве примера использования GPUImage для эффекта Blur можно рассмотреть компонент DLCImagePickerController.

Rasterize в CALayer

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

У объектов класса CALayer и их потомков есть булево свойство под названием shouldRasterize. Если оно установлено в YES, то bitmap-изображение объекта CALayer будет сохранено в кэш и далее использовано вместо самого объекта. Этот прием может помочь вам сократить затраты ресурсов процессора на перерисовку слоя в каждом кадре анимации, при этом затратив больше оперативной памяти под кэш. Есть и другие тонкости, такие как пикселизация изображения слоя в этом случае; подробнее о действии данного свойства можно узнать, просмотрев видео WWDC2010 Session 425 «Core Animation in Practice, Part 2», или ознакомившись с официальной документацией.

Существует также еще одно свойство, связанное с растеризацией изображения слоя. Оно называется rasterizationScale и, как очевидно следует из названия, отвечает за масштабирование изображения при растеризации. Придавая ему разные значения, можно заметить, что картинка становится размытой. Однако это не похоже на обычный эффект Blur. При том эффекте размытия, который мы ожидаем и рассматриваем в данной статье, все элементы изображения остаются на своих местах, просто они выглядят смазанными. Однако в случае описанного способа картинка изменяется, сначала становясь все более пикселизированной, а затем и вовсе вынося часть изображения за пределы отрисовываемой области. Получаемый эффект изображен на картинках ниже.

Без сомнений, это не Blur, однако мы все же советуем присмотреться к полученному эффекту — быть может, вы найдете ему применение в своем приложении.

Сравнение фреймворков

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

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

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

Тест с анимацией

В тесте с анимацией мы использовали фреймворки для размытия UIView, содержащего в себе картинку, которая движется и меняет масштаб, а также цветной квадрат, который только движется. Анимация выполнена не стандартными средствами Core Animation, а вручную с помощью объекта CADisplayLink, вызывающего функцию перерисовки с частотой обновления экрана. Результаты, полученные для различных устройств результате теста с анимированным содержимым, приведены в таблице ниже.

Device\Framework Core Image vImage GPUImage
iPad 2 8-12 11-15 10-12
iPad New 2-4 3-4 15-22
iPad 4 4-7 5-8 17-22
iPad mini 8-13 10-15 10-12

В первую очередь стоит отметить, что результаты, показанные всеми фреймворками, довольно низки. Для плавной анимации требуется FPS не меньше 60, и как вы можете видеть из таблицы, ни один фреймворк не достиг подобной производительности. Это вовсе не означает, что фреймворки сами по себе плохо справляются с задачей — из анализа с профилировщиком мы установили, что практически половину времени приложение тратит на получение изображения элемента UIView с частотой обновления экрана. Это означает, что если оптимизировать получение изображений для размытия, то возможно добиться куда более высоких результатов. Также из таблицы видим, что наиболее выигрышным является фреймворк GPUImage. По какой причине он демонстрирует такое ускорение по сравнению с остальными? Дело в том, что для его работы не требуется явно получать и передавать ему изображение элемента, как это уже было описано ранее. Конечно, на самом деле в его реализации тоже используется метод renderInContext:, но его использование хорошо оптимизировано за счет использования GPU. Этот фреймворк как раз и является демонстрацией того, каких улучшений можно добиться при оптимизации получения картинки.

Однако есть у него и свои недостатки. Для получения качественного эффекта blur в GPUImage нужно повышать количество проходов фильтра по изображению. Это приводит к падению производительности, хоть и незначительному.

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

Тест с областью размытия

Второй тест состоял в наложении эффекта blur на небольшую передвигающуюся область на статичной картинке. Этот тест гораздо проще по нагрузке, так как требует только одного вызова метода renderInContext:. Поэтому он является наглядной демонстрацией того, насколько сильно вызовы этого метода влияют на производительность в данной задаче. Результаты второго теста приведены в таблице ниже.

Device\Framework Core Image vImage GPUImage
iPad 2 48-60 60 60
iPad New 48-60 60 60
iPad 4 60 60 60
iPad mini 48-60 60 60
iPhone 5 60 60 60

Выводы очевидны — все фреймворки прекрасно справились, небольшое ухудшение производительности наблюдается лишь у Core Image при усилении уровня размытия.

На этом наше исследование можно считать законченным. Надеемся, что вам было также интересно, как и нам, — и вы почерпнули много полезного для себя из этого материала. Приятной разработки!)