Използвайте Metal и Core Image, за да приложите бързи и ефективни филтри за камерата на вашето приложение

Всички сме виждали персонализирани камери под една или друга форма в iOS. Обикновено бихте искали да внедрите свой собствен, за да обвиете персонален потребителски интерфейс около него, вместо да използвате опцията за вградена камера на Apple. Но как да направим персонализирана камера една крачка напред? Филтри! Много приложения използват филтри на своите камери за разширена функционалност.

В този урок ще разгледаме как да добавите филтри към вашата камера. Този урок има за цел да ви запознае с бърз и ефективен начин за филтриране на видео емисиите на живо от вашата камера и снимките, които прави.

Този урок предполага, че имате работещо разбиране за това как да настроите камера в iOS. Не е така? Не се притеснявайте, можете да прочетете предишния ми урок. Всъщност този урок се основава директно върху готовия код от този урок: „„Създаване на персонализирана камера в iOS“.“

Стартов код

Стартовият код може да бъде намерен в моя GitHub: barbulescualex/iOSMetalCamera.

Ако стартирате приложението, ще видите, че имаме много елементарна настройка на камерата. Използваме стандарта wideAngleCamera както за предната, така и за задната камера. Сесията за заснемане използва предварително зададената снимка и няма конфигурации на самите устройства за заснемане. Използваме слой за визуализация, за да покажем емисията на живо от сесията за заснемане. И накрая, ние правим снимката, използвайки видео кадри, които получаваме обратно от обекта за изход на видео данни.

Ако никога не сте използвали AVCaptureVideoDataOutput, всичко, което прави, е да връща видеокадри от сесията за заснемане, които можете да използвате, за да конвертирате в UIImage, когато потребителят направи снимка.

Настройката на потребителския интерфейс се извършва в ViewController+Extras.swift, а цялата основна логика се извършва в ViewController.swift. Отделете няколко минути, за да си поиграете с него и да разгледате настройката на кода.

Фоново мислене

Има два начина да направите снимка от сесия за заснемане. Първият начин е да използвате AVPhotoCaptureOutput, изходен обект, който прави наистина лесно да направите снимка от камерата. И вторият начин е да използваме AVCaptureVideoDataOutput, където получаваме необработените видео данни, от които можем да направим снимка, т.е. да заснемем един кадър.

Сега нека помислим за секунда. Текущата настройка използва AVCaptureVideoDataOutput, за да върне необработените видео данни, а също така използваме AVCaptureVideoPreviewLayer, за да покажем на потребителя какво вижда камерата.

В този урок ние търсим да приложим персонализирани филтри към камерата. Това, разбира се, може да се направи, след като потребителят натисне бутона на камерата, но вие искате потребителят да може да вижда ефектите през камерата. Единствената ни възможност тук е, че вместо да разчитаме на AVCaptureVideoPreviewLayer, ние разчитаме на AVCaptureVideoDataOutput да обработва кадрите с нашите ефекти и да ги представя на потребителя в реално време.

Така че стандартният слой за визуализация + изход за заснемане на снимка е добър, когато всичко, от което наистина се нуждаете, е потребителски интерфейс на камерата. Но използването на изходните видео данни, за да поемете и двете роли, е добро, когато трябва действително да обработите и приложите ефекти към камерата.

CIFilters+Metal срещу изцяло метал

Ако не сте запознати с Metal, това е рамката за използване на GPU на устройства с Cocoa. Металният изглед може да се разглежда като изглед, който показва това, което GPU изобразява. Сега всички изгледи на iOS се изобразяват с помощта на GPU, но металният изглед е специален, защото вие контролирате това, което се изобразява на много по-ниско ниво.

Ако не сте запознати с CIFilters, те са буквално просто обекти, които прилагат филтри към изображение (да, толкова е лесно!).

И така, как да приложим ефекти? Има много начини да направите това.

  • Използвайте предварително дефинираните CIFilters. Това са изпечените plug-and-chug обекти.
  • Създайте свой собствен CIFilter с помощта на Metal Shading Language. Това е същото като вградените филтри, с изключение на това, че всъщност сами пишете кода за филтъра. Metal Shading Language е начинът, по който пишете свои собствени инструкции за използване от GPU в Metal framework.
  • Направете свои собствени текстури с помощта на метал. Това е същата концепция като по-горе, но можете да правите повече неща, тъй като обхватът ви се разширява отвъд простото добавяне на филтри.

И двата начина (CIFilters или Metal) използват текстури за изобразяване в метален изглед. Разликата е как обработвате изображенията. CIFilters е по-лесна абстракция от по-високо ниво, която се доставя предварително с много филтри. Но както споменах, можете да създадете свой собствен. Използването на целия метал ще ви даде повече гъвкавост и може да бъде по-полезно в приложения, където трябва да рендирате неща върху самото изображение (вместо само да добавяте филтър към изображението).

От гледна точка на ефективността, какво искаме от това? Искаме цялата обработка да се извършва на GPU. Трябва да е очевидно, че изцяло металният подход ни дава това по същество, но подходът CIFilters+Metal ще ни даде почти същата производителност.

Част 1. Показване на рамките чрез MetalKit

В тази първа част ще се отдалечим от AVCaptureVideoPreviewLayer и сами ще покажем резултата.

Първо, нека помислим какво правим с тези рамки с данни. Когато потребителят натисне бутона на камерата, ние създаваме UIImage от тази рамка. Наивен подход за показване на кадрите е да имате UIImageView, в който актуализирате изображението за всеки кадър. Сега това очевидно е ужасно за производителността. Конвейерът на дисплея по този начин минава CPU (кадър, който получихме обратно) -› GPU (CIImage) -› CPU (UIImage). Ето защо изглед MetalKit е най-добрият начин да се справите с това.

Няма да обяснявам Metal супер задълбочено тук, защото вече имам урок за това, ако се интересувате да научите повече: „„Как да направите своя първи кръг с помощта на метални шейдъри“.“

Секция 1. Настройване на металния изглед

Имаме основен шаблон за приложение за камера, използвайки стандартния слой за визуализация. Сега искаме да премахнем това за различен тип изглед, който ще използваме, MTKView.

„Класът MTKView осигурява имплементация по подразбиране на изглед, съобразен с Metal, който можете да използвате за изобразяване на графики с помощта на Metal и показването им на екрана.“

Както можете да заключите от описанието, ние по същество ще рендираме филтрираните рамки в този изглед.

В ViewController.swift ще премахнем setupPreviewLayer() и ще добавим променлива за екземпляр, за да съхраним нашия метален изглед. И накрая, за да използвате Metal вътре в приложение, трябва да импортирате MetalKit.

В ViewController.swift+Extras.swift просто добавихме металния изглед към йерархията на изгледите и го направихме да обхваща целия екран.

Забележете как това е просто подклас на UIView. Не са необходими специални съображения, за да го добавите към потребителския интерфейс.

В този момент, ако стартирате приложението, все още можете да правите снимки, тъй като всичко работи; просто вече нямаме слой за визуализация.

Сега трябва всъщност да настроим самия метален изглед.

Свържете се с GPU на устройството

Първата стъпка е, че металният изглед работи чрез изобразяване на съдържание и това съдържание се изобразява на екрана с помощта на GPU. Графичният процесор в рамката MetalKit се представя от MTLDevice, подобно на начина, по който камера или микрофон се представя от AVCaptureDevice в рамката AVFoundation.

Кажете на MTKView как да се актуализира

Това може да се намери под „Конфигуриране на поведението при рисуванев документацията за MTKView. Искаме да актуализираме MTKView всеки път, когато има нов кадър за показване. Така че ще използваме опцията „изрично рисуване“.

Създаване на опашка с команди

За да изпратим инструкции към графичния процесор за обработка, имаме нужда от конвейер за изпращане на инструкции надолу. Този конвейер е създаден от MTLCommandQueue.

„Обект MTLCommandQueue се използва за подреждане на подреден списък от командни буфери за MTLDevice за изпълнение.“

Вие създавате тази опашка от команди от обекта MTLDevice.

Изпратете инструкции до GPU

Сега, когато имаме опашка директно към графичния процесор, как да изпращаме команди към него? Къде да му изпращаме команди? Е, MTKView има MTKViewDelegate, чиято цел е да реагира на събитията на чертане на изгледа. Тук всъщност изпращаме командите.

Първата функция, с която се съобразяваме, ни казва, че възможността за рисуване на нашия метален изглед се е променила. Какво е теглене?

MTLDrawable

„Ресурс с възможност за показване, който може да бъде изобразен или написан на... способен да показва съдържание на екрана. Използвате рисувани обекти, когато искате да рендирате изображения с помощта на Metal и да ги представите на екрана.“

Металният изглед ще има обект по подразбиране, който може да бъде изчертан, прикрепен към него, тъй като скоро ще видим, че можем да изобразим нашия CIImage (който правим от видеокадъра, който получаваме обратно от нашата камера) в него.

Сега, ако имате някакъв опит с използването на Metal, обикновено следващата стъпка ще бъде да направите конвейер с дескриптор за преминаване на рендиране и енкодер на команди, за да можете да кодирате инструкции в буфера. Тъй като работим с CIImages, можем да използваме CIContext, за да записваме команди в буфера вместо това, да!

Раздел 2. Показване на емисията на камерата през MTKView

Това връща спомените. Бях написал тази публикация в Stack Overflow през 2018 г., когато използвах UIImageView за показване на емисията на камерата и използвах CIFilters неправилно: „Объркване относно CIContext, OpenGL и Metal (SWIFT). CIContext използва ли CPU или GPU по подразбиране?

Както потребителят за препълване на стека DFD спомена в отговора си, CIImage (това нещо, което създаваме от видеокадъра) е просто рецепта за изображение, всеки път, когато преобразуваме в CGImage и UIImage, ние сме се сблъскали с процесора, така че искаме да правите това само ако е необходимо. За нас това е необходимо само когато потребителят наистина направи изображението. Ще съхраним препратка към CIImage, което вече сме създали, и ще извикаме draw в нашия метален изглед, за да поемем и изобразим изображението в металния изглед.

CoreImage е рамка за обработка и анализ на изображения. Има свое собствено изображение, наречено CIImage, което е рецепта за изображение. Върху тази рецепта за изображение можете да приложите CIFilters, което ще направим в следващата част. Третият основен клас на рамката CoreImage е CIContext.

CIContext

„Оценъчен контекст за изобразяване на резултати от обработка на изображения и извършване на анализ на изображения.“

Тази първа ключова точка е това, от което наистина се интересуваме, тоест изобразяването на изображението. И както се оказа, можем да изобразим директно метална текстура. Какво е метална текстура? Ще разгледаме това скоро.

Първо нека създадем такъв. Има няколко начина за инициализиране на CIContext. Един от тези начини е използването на метал. Това по същество му казва какво GPU устройство да използва за своите вградени функции за обработка и оценка.

След това трябва да съхраним препратка към всеки кадър в нашия клас, така че когато извикаме рисуване върху металния изглед, той да знае какъв кадър трябва да използва.

Как да направим CIImage в нашия метален изглед? CIContext има цяла група функции за изобразяване на изображения и сред тях срещаме една, която можем да използваме.

func render(_ image: CIImage, 
         to texture: MTLTexture, 
      commandBuffer: MTLCommandBuffer?, 
             bounds: CGRect, 
         colorSpace: CGColorSpace)

Нека анализираме неговите параметри, за да знаем как да продължим.

  • image — Това е просто; това е CIImage, което създаваме за всеки кадър.
  • texture — Казах, че го изобразяваме на екрана през металния изглед и споменах, че ще го използваме за рисуване. Това, което всъщност „рисуваме“, е текстурата на чертаемото, което металният изглед помещава. Текстурата в смисъл на GPU е изображение, което се използва за нанасяне върху обект. Помислете за текстурни пакети във видеоигрите.
  • commandBuffer — По-рано създадохме опашка с команди, за да можем да изпращаме инструкции към GPU. Тези „инструкции“ се представят като командни буфери и се създават от опашката с команди.
  • bounds — Това е GCRect за рисуване на изображението върху текстурата.
  • colorSpace — Това казва на CIContext как да интерпретира информацията за цвета от CIImage. За нас това са само стандартните RGB цветове.

Тук се случва доста неща.

На първо място, в нашето заснемане на изхода, където връщаме видео кадрите, вместо да се връщаме веднага, ако потребителят не е натиснал бутона на камерата, ние вместо това първо съхраняваме CIImage и след това извикваме рисуване на нашия метален изглед.

Във функцията за теглене правим точно това, за което говорихме преди. Създаваме команден буфер от опашката за команди, в който да кодираме инструкциите. Уверяваме се, че имаме изображение и металният изглед е годен за работа. След това извикваме функцията за изобразяване на контекста, като предаваме необходимите параметри. След това казваме на командния буфер къде да представи своите резултати и да го ангажираме за изпълнение.

И накрая, вътре в метала за настройка задаваме свойство framebufferOnly на true, което ни позволява да пишем директно върху текстурата на изчертаемия обект на металния изглед.

Сега, ако го стартираме, ще изобразим кадрите си на екрана и най-хубавото е, че е бързо! Разглеждайки използването на ресурсите, виждаме, че се представя наистина добре. Има малък проблем с потребителския интерфейс: видеопотокът не е центриран правилно. Залепнал е на дъното. Това може лесно да се промени чрез преместване нагоре в началната точка y, така че да е центрирана.

Вече перфектно възпроизведохме AVVideoPreviewLayer с помощта на метал, без компромис с производителността.

Част 2. Добавете CIFilters

Следващата част е доста кратка - настройването на металния изглед е най-трудната част. Стъпките ни, за да пренесем рамката от камерата на екрана досега, бяха CMSampleBuffer(от обратното извикване AVVideoCaptureDateOutput) -› CIImage -› MTLTexture.

CIImage, който създаваме от видео рамката, се използва както за наш собствен персонализиран преглед на видео с помощта на Metal, така и за запазване на изображението, което потребителят прави. Ако искаме да приложим филтрите, всичко, което трябва да направим, е да ги приложим, преди да използваме CIImage за изобразяване на екрана и/или преди да го запазим, когато потребителят направи снимка.

CIFilter

„Процесор за изображения, който създава изображение чрез манипулиране на едно или повече входни изображения или чрез генериране на нови данни за изображение.

„Класът CIFilter произвежда CIImage обект като изход.“

Тази стъпка не може да бъде по-лесна. CIFilters са абстракционни класове на високо ниво за добавяне на филтри към изображения и вече има много предварително изпечени опции!

Всеки филтър приема поне един параметър (a CIImage). Някои филтри имат допълнителни входни параметри като интензитет, радиус и т.н. Тъй като тези филтри имат толкова много опции, всички параметри приемат низове, описващи кой филтър да се използва и какви входове искате да промените.

Core Image Filter Referenceсъдържа цялата необходима информация. Има всички налични предварително дефинирани филтри, така че можете да създавате филтрите по име. Освен това има всички опции за въвеждане за всеки филтър, така че можете да ги настроите.

За да ви покажем колко е лесно, ще има само един кодов фрагмент и сме готови!

Както можете да видите, направихме много малко промени. Декларирахме два филтъра и ги настроихме, когато настроихме нашия CIContext. След това декларирахме нова функция, която приема CIImage и свързва два филтъра, като задава изображението като вход във филтъра и след това извлича филтрираното изображение от филтъра. И вътре в captureOutput обратното извикване заменяме CIImage, което използвахме преди, с филтрирано.

Избрах да комбинирам филтър за избледняване и филтър за сепия, за да създам хубав винтидж тон.

Ако стартирате приложението, вече имате своя собствена, бърза и ефективна персонализирана камера в iOS.

Пълният код на частта може да бъде намерен в моя GitHub: barbulescualex/iOSMetalCamera.

Следващи стъпки

Къде можете да отидете от тук?

  • Вградете потребителски интерфейс за промяна на филтрите. Това може да бъде всичко - от избор на различни филтри с предварително зададени входни параметри до наличие на плъзгачи, за да можете да променяте параметрите на филтъра.
  • Видеоклипове! Всеки кадър, който получаваме обратно от камерата, филтрираме и го показваме в металния изглед. Заснемането на видео, макар и да не е тривиално, също не е твърде трудно. Всичко, което предполага, е, че обединявате видео кадрите във файл. Това също е чудесна възможност да изследвате използването на аудио устройства в iOS.
  • Персонализирани CIFilters. Ако не сте доволни от предложенията на Apple, защо не направите свои собствени?

Заключение

Ако ви е харесал този урок и бихте искали да научите повече за Metal, вижте моето въведение в използването на Metal shaders, „„Как да направите своя първи кръг с помощта на Metal Shaders“.“

Вече сте запознати с Metal, но искате да видите как можете да го използвате, за да правите страхотни неща? Вижте моя урок за аудио визуализация, „Аудиовизуализация в Swift, използваща Metal и Accelerate (част 1).“

Както винаги, ако имате въпроси или коментари, не се колебайте да ги оставите по-долу.