Instagram запустился в октябре 2010. За 18 месяцев он изменил ожидания пользователей от мобильной камеры: фильтры для фото перешли из разряда новинок в обязательный элемент.
Каждый клиент с lifestyle-приложением хотел фильтры. «Как в Instagram, но для [нашей ниши]». Нам пришлось быстро изучить обработку изображений. Вот что мы на самом деле построили.
Математика пикселей
Каждый фильтр - это преобразование значений пикселей. JPEG-изображение - это сетка пикселей; каждый пиксель имеет значения красного, зелёного и синего каналов от 0 до 255.
До iOS 5 с Core Image (GPU-ускоренным фреймворком) мы обрабатывали изображения на CPU в циклах:
- (UIImage *)applyGrayscale:(UIImage *)image {
// ... получение пиксельного буфера ...
for (NSInteger y = 0; y < height; y++) {
for (NSInteger x = 0; x < width; x++) {
NSInteger byteIndex = (bytesPerRow * y) + (x * bytesPerPixel);
unsigned char r = rawData[byteIndex];
unsigned char g = rawData[byteIndex + 1];
unsigned char b = rawData[byteIndex + 2];
// Формула яркостного перевода в оттенки серого
unsigned char gray = (unsigned char)(0.299 * r + 0.587 * g + 0.114 * b);
rawData[byteIndex] = gray;
rawData[byteIndex + 1] = gray;
rawData[byteIndex + 2] = gray;
}
}
// ... создание нового изображения ...
}
На фото 2048×1536 пикселей (задняя камера iPhone 4) этот цикл проходил 3 145 728 пикселей. На чипе A4 это занимало 1.2 секунды. Приемлемо в фоновом потоке с индикатором загрузки. Неприемлемо для предварительного просмотра в реальном времени.
Деконструкция «vintage»-вида
Мы разобрали по пикселям, что делает фото похожим на Instagram-vintage. Эффект «старой фотографии» получался наложением нескольких независимых трансформаций:
1. Частичное обесцвечивание
float saturationFactor = 0.6; // 0 = полное Ч/Б, 1 = оригинал
float grayValue = 0.299 * r + 0.587 * g + 0.114 * b;
r = (unsigned char)(grayValue + saturationFactor * (r - grayValue));
g = (unsigned char)(grayValue + saturationFactor * (g - grayValue));
b = (unsigned char)(grayValue + saturationFactor * (b - grayValue));
2. Тёплый цветовой сдвиг
r = MIN(255, r + 20); // Поднимаем красный
b = MAX(0, b - 15); // Опускаем синий
3. Кривая контраста (S-кривая)
// Предвычисленная таблица поиска - очень быстро в цикле
unsigned char lutTable[256];
for (int i = 0; i < 256; i++) {
float t = i / 255.0f;
float curved = t * t * (3.0f - 2.0f * t); // Кубическая S-кривая
float result = t + 0.5f * (curved - t);
lutTable[i] = (unsigned char)(CLAMP(result, 0.0f, 1.0f) * 255.0f);
}
r = lutTable[r]; g = lutTable[g]; b = lutTable[b];
4. Виньетка (тёмные края как у старой оптики)
float cx = (float)x / width - 0.5f;
float cy = (float)y / height - 0.5f;
float dist = sqrtf(cx * cx + cy * cy);
float vignette = 1.0f - CLAMP(dist * 1.4f - 0.3f, 0.0f, 0.8f);
r = (unsigned char)(r * vignette);
g = (unsigned char)(g * vignette);
b = (unsigned char)(b * vignette);
5. Зернистость плёнки - случайный шум в диапазоне ±8 на каждый канал.
Комбинация этих пяти операций давала что-то, что реально напоминало выцветшую фотографию 1970-х.
Core Image: GPU-ускорение (iOS 5)
Apple's Core Image переместил обработку фильтров на GPU. Та же операция обесцвечивания, которая занимала 1.2 секунды на CPU, выполнялась за 4мс на GPU:
- (UIImage *)applyCIVintage:(UIImage *)inputImage {
CIImage *ciImage = [CIImage imageWithCGImage:inputImage.CGImage];
CIContext *context = [CIContext contextWithOptions:nil];
CIFilter *saturationFilter = [CIFilter filterWithName:@"CIColorControls"];
[saturationFilter setValue:ciImage forKey:kCIInputImageKey];
[saturationFilter setValue:@0.6 forKey:kCIInputSaturationKey];
CIFilter *vignetteFilter = [CIFilter filterWithName:@"CIVignette"];
[vignetteFilter setValue:saturationFilter.outputImage forKey:kCIInputImageKey];
[vignetteFilter setValue:@1.5 forKey:kCIInputIntensityKey];
CGImageRef cgResult = [context createCGImage:vignetteFilter.outputImage
fromRect:vignetteFilter.outputImage.extent];
return [UIImage imageWithCGImage:cgResult];
}
Предварительный просмотр в реальном времени стал возможен: применяем фильтры к уменьшенной превьюшке (200×150) для предварительного просмотра, а полную обработку запускаем только при подтверждении выбора.
Чему нас научила обработка изображений
Алгоритмы пикселей - зажим, линейная интерполяция, кривые, свёртки - существуют в другом домене по сравнению с обычной бизнес-логикой.
Урок с GPU был фундаментальным: некоторые вычисления параллельны (каждый пиксель независим) и принадлежат GPU, а не CPU. Это мышление позже перешло в работу с WebGL, GLSL-шейдерами в 2014-2016 годах, и в итоге - в машинное обучение на GPU.
Гениальность Instagram была не в математике фильтров - математика 40-летней давности. Гениальность была в UX: одно нажатие, которое превращало посредственное фото в нечто достойное публикации. Техническая работа состояла в том, чтобы это одно нажатие ощущалось мгновенным.