Как разработать свой фоторедактор для iOs. Отчет по конкурсу ВКонтакте

в 5:21, , рубрики: photoshop, Вконтакте, конкурс, разработка под iOS, фотография, метки: , , ,

Как разработать свой фоторедактор для iOs. Отчет по конкурсу ВКонтакте
Привет всем хабражителям и интересующимся!
Вчера (внезапно) случился конец сдачи проектов на первый этап еще одиного славного конкурса фоторедакторов для iOs от ВКонтакте. И в этой статье я хочу поделиться приобретенным опытом, рассказать о граблях и проблемах, с которыми я столкнулся при разработке своей версии сего продукта.

«Движок»

Требование конкурса для съемки: «Все фильтры должны работать в реальном времени и не замедлять работу приложения» — породило вопрос, «какой же взять движок». Решение этого вопроса гуглится за 5 секунд и называется GPUImage. Один мой друг (не будем называть его имени) сказал, что нафиг его, там 140 открытых issue, и вообще проще все взять и написать самому. Но времени было жалко, к тому же, я оцениваю свои силы объективно, поэтому взял именно его. Думаю, почти все участники используют именно эту библиотеку :)
Конечно же, проблем с GPUImage возникало много, но вроде все решаемые.
Например, большая проблема с потреблением памяти, которая утекает в неизвестном направлении, из-за чего приложение закрывается. Это может быть как проблема вашего кода, так и проблема библиотеки — не совсем понятно. Хоть примеров в библиотеке целая куча, однако некоторые тонкие моменты могут стать головной болью.
Так, у меня в код закралась ошибка, когда я делал что-то вроде:

[stillCamera        addTarget:filter];

или

[filter prepareForImageCapture];

два раза, из-за чего приложение потребляло космические масштабы памяти и падало на съемке (особенно с блюром).
Так же проблема заключается в том, что все фильтры генерируют внутри себя текстуры высоких разрешений, поэтому я решил, что 640kb хватит всем 1024x1536 пикселей хватит всем, и сделал принудительную обработку к заданному размеру:

CGSize forceSize = CGSizeMake(1024, 1024 * 1.5);
        if (!frontCameraSelected)
            [processFilter forceProcessingAtSize:forceSize];

        @autoreleasepool {
            [stillCamera capturePhotoAsJPEGProcessedUpToFilter:processFilter ...
...

Однако, должен признать, что за эти две недели разработчик библиотеки Brad Larson исправил несколько проблем, и вообще постоянно держит связь. Словом, молодец!

Фильтры

Как разработать свой фоторедактор для iOs. Отчет по конкурсу ВКонтакте
Пожалуй, это наиболее сложная часть, с точки зрения программиста, ведь все представленные фильтры необходимо было подобрать. Для подбора фильтров, я написал дополнительно приложение на iPad, с помощью которого и подбирал фильтры с параметрами, сразу используемыми для GPUImage.

Примерный процесс подбора фильтров:
Как разработать свой фоторедактор для iOs. Отчет по конкурсу ВКонтакте

Я потратил примерно 3 дня (по 1-2 часа) на подбор представленных фильтров, а потом стал развлекаться с дополнительными. Например, восьмибитный фильтр мой любимый:
Как разработать свой фоторедактор для iOs. Отчет по конкурсу ВКонтакте

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

-(void)getFilters
{
    /*
     Contrast : 1.032491
     Gamma : 1.196751
     Sepia : 0.442238
     Saturation : 1.198556
     */
    GPUImageContrastFilter * contrast = [[GPUImageContrastFilter alloc] init];
    contrast.contrast = 1.032491f;
    [self prepare:contrast];
    
    GPUImageGammaFilter * gamma = [[GPUImageGammaFilter alloc] init];
    gamma.gamma = 1.196751;
    [self prepare:gamma];
    
    GPUImageSepiaFilter * sepia = [[GPUImageSepiaFilter alloc] init];
    sepia.intensity = 0.442238;
    [self prepare:sepia];
    
    GPUImageSaturationFilter * saturation = [[GPUImageSaturationFilter alloc] init];
    saturation.saturation = 1.198556;
    [self prepare:saturation];    
}

Затем с фильтрами возникает новая беда: тормознутость в реальном времени :) к сожалению, для многих фильтров я не смог ее до конца победить, хотя была идея не объединять фильтры в группы, а сразу объединить их в один шейдер. Я попробовал с одним, но не выиграл большой производительности, поэтому решил пожалеть время и не использовать такой подход.

Масштабирование

Одним из условий конкурса являлось мастабирование и подгонка изображения. UIScrollView очень хорошо работает с масштабированием, но ведь нам нужно «снимать результат» в GPUImageView. Я пошел на хитрость, или уловку, и применял к изображению фильтр трансформации GPUImageTransformFilter. Трансформацию же считал по результатам масштабирования и перетаскивания UIScrollView, которая лежит на верхнем слое.
Код для трансформации получился такой:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    CGPoint offset = scrollView.contentOffset;
    CGSize size = scrollView.contentSize;
    CGFloat scrollViewWidth = scrollView.frame.size.width,
            scrollViewHeight = scrollView.frame.size.height;
    translationX = 0;
    float a = size.width - scrollViewWidth, b = size.height - scrollViewWidth;
    if ((int)a != 0) {
        translationX = (a / scrollViewWidth) * (0.5f - offset.x / a) * 2;
    }
    translationY = 0;
    if ((int)b != 0) {
        translationY = (b / scrollViewWidth) * (0.5f - offset.y / b) * 2;
    }
    if (size.height < size.width)
    {
        translationX *= aspectRatio;
        translationY *= aspectRatio;
    }
    CGAffineTransform resizeTransform = CGAffineTransformMakeScale(scrollView.zoomScale / scrollView.minimumZoomScale, scrollView.zoomScale / scrollView.minimumZoomScale);
    resizeTransform.tx = translationX;
    resizeTransform.ty =  translationY;
    transformFilter.affineTransform = resizeTransform;
    [self fastRedraw]; //Обновление сцены
}

Для меня, пожалуй, самое странное в этом явилось то, что когда изображение у нас горизонтальное, то мы должны результат умножать на соотношение сторон. Честно, я это больше подобрал, чем осознал.
Кроме того, масштабирование и перетаскивание с применённым фильтром не очень хорошая идея, потому что тормозит зверски. Поэтому я отключал фильтр на время этих действий, и включал после. Работает просто чудесно.

Поддержка iPhone 5

Это не очень сложная тема, однако, нужно иметь ее ввиду. Приложение не просто должно растягиваться, но и вести себя немного иначе. К счастью, autoresize решает 80% проблем, остальные 20% решает код с использованием одного уже широко известного метода:

- (BOOL)hasFourInchDisplay {
    return ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone && [UIScreen mainScreen].bounds.size.height == 568.0);
}

В важных местах используйте этот код, и подгоняйте анимации и рамки под новые размеры. И все у вас будет хорошо. Просто нужно помнить о новом iPhone, хотя бы симуляторе.

Recent photos

За день, когда всё, практически, было готово, новой головной болью стали «прошлые фотографии». Их проблема в том, что необходимо обновлять их своевременно: сделал фотку — обнови, удалил из галереи — обнови.
Не знаю, как остальные участники, но я делал получение недавних фоток с помощью AssetsLibrary и метода enumerateAssetsAtIndexes ... . Так вот этот метод падал, когда вы загружаете ваши ассеты, потом выходите из приложения, удаляете что-нибудь из галереи, а потом снова входите в приложение, потому что в [NSIndexSet indexSetWithIndexesInRange:assetsRange] уже хранится невалидный сет.
В общем, до самых последних часов сдачи, эта проблема меня мучила, но теперь она исправлена.

Вместо эпилога

За эти две недели, я получил много экспы, и разобрался со многими интересными аспектами, как в разработке, программировании, так и в обработке изображений, и вообще работе с подобными библиотеками.
Хотелось бы пожелать всем участникам удачи, и призовых мест! А мне — первое ;)

P.S. Исходный код выложу после публикации результатов конкурса. Чтобы не было эксцессов.

Автор: Dreddik

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js