Статьи

iOS SDK дополненной реальности: обработка видео

Добро пожаловать в последний выпуск нашей премиальной серии, посвященной дополнению реальности с помощью iOS SDK! В сегодняшнем уроке я научу вас, как обрабатывать и анализировать потоковое видео в реальном времени с камеры устройства, чтобы улучшить наше представление о мире с помощью полезной оверлейной информации.


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


В последние годы термин «дополненная реальность» стал модной фразой, поскольку смартфоны стали достаточно мощными, чтобы поместить эту технологию в наши карманы. К сожалению, вся реклама вокруг этого термина породила много недоразумений относительно того, что такое дополненная реальность и как ее можно использовать для улучшения нашего взаимодействия с миром. Чтобы уточнить, цель приложения дополненной реальности должна состоять в том, чтобы взять существующее представление пользователя или восприятие мира и усилить это восприятие, предоставляя дополнительную информацию или перспективы, которые не являются естественными. Одной из самых ранних и практичных реализаций дополненной реальности является основной момент «First Down», который обычно можно увидеть при просмотре игр по американскому футболу по телевизору. Тонкая природа этого наложения — именно то, что делает его идеальным использованием AR. Техника не отвлекает пользователя, но его взгляд естественно улучшается.

Демо-версия дополненной реальности, которую мы будем создавать сегодня, очень тематически похожа на систему Football first and 10. Мы будем обрабатывать каждый видеокадр с камеры и вычислять средний цвет RGB этого кадра. Затем мы отобразим результат в виде наложения RGB, Hex и образца цвета. Это небольшое простое усовершенствование представления, но оно должно служить нашей цели — показать, как получить доступ к видеопотоку с камеры и обработать его с помощью собственного алгоритма. Итак, начнем!


Для завершения этого урока нам понадобится еще несколько фреймворков.

Выполните шаги, описанные в шаге 1 в предыдущем руководстве этой серии, импортируйте эти платформы:

  • Как следует из названия, эта структура используется для обработки видео и в первую очередь отвечает за поддержку буферизации видео. Основным типом данных, который нас интересует в этой CVImageBufferRef , является CVImageBufferRef , который будет использоваться для доступа к буферизованным данным изображения из нашего видеопотока. Мы также будем использовать множество различных функций CoreVideo, включая CVPixelBufferGetBytesPerRow() и CVPixelBufferLockBaseAddress() . Каждый раз, когда вы видите «CV», добавленный к типу данных или имени функции, вы будете знать, что оно пришло из CoreVideo.

  • Core Media обеспечивает низкоуровневую поддержку, на которой построена платформа AV Foundation (добавлена ​​в последнем руководстве). Мы действительно просто заинтересованы в CMSampleBufferRef данных CMSampleBufferRef и функции CMSampleBufferGetImageBuffer() из этого фреймворка.

  • Quartz Core отвечает за большую часть анимации, которую вы видите при использовании устройства iOS. Нам нужно это только в нашем проекте по одной причине, CADisplayLink . Подробнее об этом позже в уроке.

В дополнение к этим платформам нам также понадобится проект UIColor Utilties для добавления удобной категории в объект UIColor . Чтобы получить этот код, вы можете посетить страницу проекта на GitHub или просто найти файлы UIColor-Expanded.h и UIColor-Expanded.m в исходном коде этого руководства. В любом случае вам нужно будет добавить оба этих файла в ваш проект Xcode, а затем импортировать категорию в ARDemoViewController.m :

1
#import «UIColor-Expanded.h»

Когда мы остановились в последнем уроке, мы реализовали слой предварительного просмотра, отображающий то, что могла видеть камера устройства, но у нас фактически не было способа получить доступ и обработать данные кадра. Первым шагом в этом является делегат AV Foundation -captureOutput:didOutputSampleBuffer:fromConnection: Этот метод делегата предоставит нам доступ к CMSampleBufferRef данных изображения для текущего видеокадра. Нам нужно будет преобразовать эти данные в полезную форму, но это будет хорошим началом.

В ARDemoViewController.h вам необходимо соответствовать правому делегату:

1
2
@interface ARDemoViewController : UIViewController
                               <AVCaptureVideoDataOutputSampleBufferDelegate>

Теперь добавьте метод делегата в ARDemoViewController.m :

1
2
3
4
— (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
      
  
}

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

01
02
03
04
05
06
07
08
09
10
11
12
// Configure capture session output
AVCaptureVideoDataOutput *videoOut = [[AVCaptureVideoDataOutput alloc] init];
[videoOut setAlwaysDiscardsLateVideoFrames:YES];
          
NSDictionary *videoSettings = [NSDictionary
                               dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA]
                                             forKey:(id)kCVPixelBufferPixelFormatTypeKey];
[videoOut setVideoSettings:videoSettings];
dispatch_queue_t color_queue = dispatch_queue_create(«com.mobiletuts.ardemo.processcolors», NULL);
[videoOut setSampleBufferDelegate:self queue:color_queue];
          
[cameraCaptureSession addOutput:videoOut];

В строке AVCaptureSession выше мы настраиваем дополнительные параметры для видеовыхода, сгенерированного нашей AVCaptureSession . В частности, мы указываем, что мы хотим получать каждый пиксельный буфер, отформатированный как kCVPixelFormatType_32BGRA , что в основном означает, что он будет возвращен как 32-битное значение, упорядоченное как синий, зеленый, красный и альфа-каналы. Это немного отличается от 32-битного RGBA, который используется чаще всего, но небольшое изменение в порядке следования каналов не замедлит нас. 🙂

Далее в строках 9-10 мы создаем очередь отправки, в которой будет обрабатываться пример буфера. Очередь необходима, чтобы предоставить нам достаточно времени для фактической обработки одного кадра до получения следующего. Хотя теоретически это означает, что наша камера может только видеть прошлое, задержка не должна быть заметна, если вы эффективно обрабатываете каждый кадр.

С указанным выше кодом ARDemoViewController должен начать получать вызовы делегатов с CMSampleBuffer каждого видеокадра!


Теперь, когда у нас есть CMSampleBuffer нам нужно преобразовать его в формат, который мы можем легче обработать. Поскольку мы работаем с Core Video, выбранный нами формат будет CVImageBufferRef . Добавьте следующую строку в метод делегата:

1
2
3
4
— (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
      
    CVImageBufferRef pixelBuffer = CMSampleBufferGetImageBuffer( sampleBuffer );
}

Хорошо, теперь пришло время начать обработку этого пиксельного буфера. Для этого урока мы создаем приложение дополненной реальности, которое может смотреть на любую сцену или картинку и сообщать нам, каковы средние значения цвета Hex и RGB в кадре. Давайте создадим новый метод findColorAverage: просто для выполнения этой задачи.

Добавьте объявление метода в ARDemoViewController.h :

1
— (void) findColorAverage: (CVImageBufferRef)pixelBuffer;

А затем добавьте заглушку реализации в ARDemoViewController.m :

1
2
3
— (void)findColorAverage: (CVImageBufferRef)pixelBuffer {
  
}

Наконец, вызовите новый метод в конце реализации делегата:

1
2
3
4
5
6
— (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
      
    CVImageBufferRef pixelBuffer = CMSampleBufferGetImageBuffer( sampleBuffer );
      
    [self findColorAverage:pixelBuffer];
}

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

Следующий код будет перебирать каждый пиксель в кадре:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
— (void)findColorAverage:(CVImageBufferRef)pixelBuffer {
      
    CVPixelBufferLockBaseAddress( pixelBuffer, 0 );
          
    int bufferHeight = CVPixelBufferGetHeight(pixelBuffer);
    int bufferWidth = CVPixelBufferGetWidth(pixelBuffer);
    int bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
  
    unsigned char *pixel;
      
    unsigned char *rowBase = (unsigned char *)CVPixelBufferGetBaseAddress(pixelBuffer);
      
    for( int row = 0; row < bufferHeight; row += 8 ) {
        for( int column = 0; column < bufferWidth; column += 8 ) {
              
            pixel = rowBase + (row * bytesPerRow) + (column * 4);
              
        }
    }
                  
    CVPixelBufferUnlockBaseAddress( pixelBuffer, 0 );
}

В строке 3 выше мы вызываем CVPixelBufferLockBaseAddress в нашем пиксельном буфере, чтобы предотвратить изменение данных во время их обработки.

В строках 5-7 мы получаем некоторую базовую мета-информацию о пиксельном буфере, а именно высоту и ширину буфера и количество байтов, хранящихся в каждой строке.

В строке 9 указатель символа используется для объявления пикселя. В C тип данных char может содержать один байт данных. По умолчанию один байт данных в C может содержать любое целое число в диапазоне от -128 до 127. Если этот байт «без знака», он может содержать целочисленное значение от 0 до 255. Почему это важно? Поскольку каждое из значений RGB, к которым мы хотим получить доступ, находится в диапазоне от 0 до 255, то есть для хранения требуется один байт. Вы также помните, что мы сконфигурировали наш видеовыход для возврата 32-битного значения в формате BGRA. Поскольку каждый байт равен 8 битам, теперь это должно иметь гораздо больше смысла: 8 (B) + 8 (G) + 8 (R) + 8 (A) = 32. Подводя итог: мы используем указатель на символ ссылаться на наши данные пикселей, потому что каждое значение RGBA содержит один байт данных.

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

Строки 13 — 14 образуют вложенный цикл, который будет перебирать каждое значение пикселя в пиксельном буфере.

В строке 16 мы фактически назначаем пиксельный буфер начальному адресу памяти текущей последовательности RGBA. Отсюда мы сможем ссылаться на каждый байт в последовательности.

Наконец, в строке 21 мы разблокируем пиксельный буфер после завершения нашей обработки.


Итерации по пиксельному буферу не принесут нам большой пользы, если мы не воспользуемся информацией. Для нашего проекта мы хотим вернуть один цвет, который представляет среднее значение всех пикселей в кадре. Начнем с добавления переменной currentColor в файл .h:

1
2
3
4
    UIColor *currentColor;
}
  
@property(nonatomic, retain) UIColor *currentColor;

Не забудьте также синтезировать это значение:

1
@synthesize currentColor;

Затем измените метод findColorAverage: следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
— (void)findColorAverage: (CVImageBufferRef)pixelBuffer {
      
    CVPixelBufferLockBaseAddress( pixelBuffer, 0 );
          
    int bufferHeight = CVPixelBufferGetHeight(pixelBuffer);
    int bufferWidth = CVPixelBufferGetWidth(pixelBuffer);
    int bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
      
    unsigned int red_sum = 0;
    unsigned int green_sum = 0;
    unsigned int blue_sum = 0;
    unsigned int alpha_sum = 0;
    unsigned int count = 0;
    unsigned char *pixel;
      
    unsigned char *rowBase = (unsigned char *)CVPixelBufferGetBaseAddress(pixelBuffer);
      
    for( int row = 0; row < bufferHeight; row += 8 ) {
        for( int column = 0; column < bufferWidth; column += 8 ) {
              
            pixel = rowBase + (row * bytesPerRow) + (column * 4);
              
            red_sum += pixel[2];
            green_sum += pixel[1];
            blue_sum += pixel[0];
            alpha_sum += pixel[3];
            count++;
              
        }
    }
                  
    CVPixelBufferUnlockBaseAddress( pixelBuffer, 0 );
      
    self.currentColor = [UIColor colorWithRed:red_sum / count / 255.0f
                                        green:green_sum / count / 255.0f
                                         blue:blue_sum / count / 255.0f
                                        alpha:1.0f];
      
}

Вы можете видеть, что мы начали наши изменения с добавления переменных red_sum , green_sum , blue_sum , alpha_sum и count . Вычисление среднего для значений пикселей выполняется так же, как вы вычисляете среднее для всего остального. Таким образом, наши переменные суммы RGBA будут содержать общую сумму каждого значения, как мы собираемся, а переменная count будет содержать общее количество пикселей, увеличиваясь каждый раз в цикле.

Назначение пикселей фактически происходит в строках 24 — 26. Поскольку наша переменная пикселей — это просто указатель на определенный байт в памяти, мы можем получить доступ к последующим адресам памяти так же, как вы могли бы ожидать, используя массив. Обратите внимание, что порядок значений BGRA — это то, что вы ожидаете от значений индекса: B = 0, G = 1, R = 2, A = 3. На самом деле мы не будем использовать альфа-значение для чего-либо полезного в этом уроке, но я включил его сюда для полноты картины.

После того, как наш вложенный цикл завершил итерацию по массиву, пришло время установить полученный в качестве текущего цвета результат. Это просто элементарная математика. Сумма каждого значения RGB делится на общее количество пикселей в изображении для генерации среднего значения. Поскольку вызов метода UIColor ожидает значения с плавающей точкой, и мы имеем дело с целыми числами, мы снова делим на 255, чтобы получить значение эквивланта в виде числа с плавающей точкой.

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


Чтобы помочь пользователям разобраться в информации, которую мы рассчитали, мы собираемся создать в целом три объекта: UILabel для хранения шестнадцатеричного представления цвета, UILabel для хранения RGB-представления цвета и UIView на самом деле отображать рассчитанный цвет.

Откройте файл ARDemoViewController.xib и добавьте две метки. Установите цвет фона для каждого на черный и цвет шрифта на белый. Это обеспечит его выделение на любом фоне. Затем установите шрифт на что-то вроде Helvetica и увеличьте его размер до 28. Мы хотим, чтобы текст был легко виден. Соедините эти метки с IBOutlets в ARDemoViewController.h , убедившись , что они также синтезированы в ARDemoViewController.m (теперь Interface Builder может сделать это для вас с помощью перетаскивания). Назовите одну метку hexLabel а другую — rgbLabel .

Находясь в Интерфейсном UIView , перетащите UIView на главный UIView и настройте его в соответствии с размером и положением по вашему выбору. Соедините представление как IBOutlet и назовите его colorSwatch .

После выполнения этого шага ваш файл XIB должен выглядеть примерно так:


Каждый добавленный объект также должен быть подключен через IBOutlets к ARDemoViewController .

Последнее, что нужно сделать, это убедиться, что эти объекты видны после того, как мы добавили слой предварительного просмотра на экран. Для этого добавьте следующие строки в -viewDidLoad в ARDemoViewController :

1
2
3
[self.view bringSubviewToFront:self.rgbLabel];
[self.view bringSubviewToFront:self.hexLabel];
[self.view bringSubviewToFront:self.colorSwatch];

Поскольку функция findColorAverage: должна выполняться как можно быстрее, чтобы предотвратить потерю последних кадров в очереди, выполнение работы интерфейса в этой функции нежелательно. Вместо этого findColorAverage: просто вычисляет средний цвет рамки и сохраняет его для дальнейшего использования. Теперь мы можем настроить вторую функцию, которая действительно будет что-то делать со значением currentColor , и мы могли бы даже разместить эту обработку в отдельном потоке, если это необходимо. Для этого проекта мы просто хотим обновить оверлей интерфейса примерно 4 раза в секунду. Мы могли бы использовать NSTimer для этой цели, но я предпочитаю использовать CADisplayLink когда это возможно, потому что он более последовательный и надежный, чем NSTimer .

В ARDemoViewController реализации ARDemoViewController добавьте следующее в метод -viewDidLoad :

1
2
3
CADisplayLink *updateTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateColorDisplay)];
[updateTimer setFrameInterval:15];
[updateTimer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

CADisplayLink будет CADisplayLink обновление 60 раз в секунду, поэтому, установив интервал кадров в 15, мы будем инициировать вызов селектора updateColorDisplay 4 раза в секунду.

Чтобы это не вызывало ошибки во время выполнения, давайте добавим заглушку селектора:

1
2
3
-(void)updateColorDisplay
{
}

Теперь мы готовы дополнить наш дисплей практически полезной информацией об окружающем нас мире! Добавьте следующие строки кода для updateColorDisplay :

1
2
3
4
5
6
7
8
9
-(void)updateColorDisplay
{
    self.colorSwatch.backgroundColor = self.currentColor;
    self.rgbLabel.text = [NSString stringWithFormat:@»R: %d G: %d B: %d»,
                          (int) ([self.currentColor red] * 255.0f),
                          (int) ([self.currentColor green] * 255.0f),
                          (int) ([self.currentColor blue] * 255.0f) ];
    self.hexLabel.text = [NSString stringWithFormat:@»#%@», [self.currentColor hexStringFromColor]];
}

То, что мы делаем выше, действительно довольно просто. Поскольку currentColor хранится как объект UIColor , мы можем просто установить для него свойство backgroundColor colorSwatch напрямую. Для обеих меток мы просто устанавливаем пользовательский формат NSString и используем категорию UIColor-Expanded для простого доступа как к шестнадцатеричному представлению цвета, так и к значениям RGB.


Чтобы проверить вашу работу, я включил 3 простых файла HTML в папку «test» загрузки проекта. Каждый файл имеет сплошной фон (красный, зеленый и синий). Если вы заполнили монитор своего компьютера открытой веб-страницей и направили наше приложение для iPhone на экран, вы должны увидеть соответствующее цветное всплывающее окно на дисплее AR.


Поздравляем! Вы создали свое первое настоящее приложение дополненной реальности!

Хотя мы, безусловно, можем обсуждать ценность информации, с которой мы расширяем наш взгляд на мир, я лично нахожу этот проект гораздо более интересным, чем большинство «дополненных» приложений дополненной реальности, имеющихся в настоящее время на рынке, включая оригинальную ». Трубы »приложение. Почему? Потому что, когда я нахожусь в городе в поисках метро, ​​я не хочу, чтобы стрелка «круг земли» указывала мне через здания к моей цели. Вместо этого гораздо практичнее просто использовать указания из Карт Google. По этой причине каждое приложение дополненной реальности с учетом местоположения, с которым я сталкивался, действительно немного больше, чем проект новизны. Хотя проект, который мы создали сегодня, также является своего рода новым проектом, я надеюсь, что это был интересный пример того, как начать создавать свои собственные приложения Augmetned Reality, которые действительно могут добавить значимую информацию к окружающему нас миру.

Если вы нашли этот урок полезным, сообщите мне в Twitter: @markhammonds . Я хотел бы увидеть, что вы придумали!


Как я уверен, вы уже догадались, что мы действительно только что коснулись поверхности того, что могут делать приложения дополненной реальности. Следующий шаг в вашем образовании во многом будет зависеть от того, чего вы хотели бы достичь с помощью приложения AR.

Если вы заинтересованы в приложениях дополненной реальности с учетом местоположения, вам стоит подумать об использовании AR-инструментария с открытым исходным кодом. Хотя вы, безусловно, можете написать свой собственный инструментарий AR, это очень сложная и сложная задача. Я бы посоветовал вам не изобретать велосипед, а вместо этого взглянуть на эти проекты:

Вот несколько дополнительных решений и проектов дополненной реальности, которые поддерживают реализации на основе маркеров:

Если вы особенно заинтересованы в обработке изображений, как мы продемонстрировали в этом уроке, и хотели бы перейти к более продвинутым функциям и распознаванию объектов, хорошие отправные точки включают:


Как я надеюсь, вы можете увидеть из содержания этого учебника, Augmented Reality — невероятно широкая тема с множеством различных направлений, от обработки изображений и распознавания объектов до определения местоположения и даже 3D-игр.

Мы, безусловно, заинтересованы в освещении всего вышеперечисленного на Mobiletuts +, но мы хотим убедиться, что наш контент соответствует тому, что читатели хотели бы видеть. Сообщите нам, в каком аспекте дополненной реальности вы хотели бы видеть больше учебных пособий, оставив комментарий на Mobiletuts + , разместив сообщение на нашей стене в Facebook или отправив мне сообщение прямо в твиттере: @markhammonds .

До скорого. , .Спасибо за прочтение!