Привет всем хабражителям и интересующимся!
Вчера (внезапно) случился конец сдачи проектов на первый этап еще одиного славного конкурса фоторедакторов для 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 исправил несколько проблем, и вообще постоянно держит связь. Словом, молодец!
Фильтры
Пожалуй, это наиболее сложная часть, с точки зрения программиста, ведь все представленные фильтры необходимо было подобрать. Для подбора фильтров, я написал дополнительно приложение на iPad, с помощью которого и подбирал фильтры с параметрами, сразу используемыми для GPUImage.
Примерный процесс подбора фильтров:
Я потратил примерно 3 дня (по 1-2 часа) на подбор представленных фильтров, а потом стал развлекаться с дополнительными. Например, восьмибитный фильтр мой любимый:
Таким образом, я создал класс фильтров, которые задавались группой. Опуская инициализацию и внутренние вызовы, выглядит это примерно вот так:
-(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