鍍金池/ 教程/ iOS/ Core Image 和視頻
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動布局工具箱
動畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應(yīng)式 Android 應(yīng)用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動畫
常見的后臺實(shí)踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學(xué)無止境
XCTest 測試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

Core Image 和視頻

在這篇文章中,我們將研究如何將 Core Image 應(yīng)用到實(shí)時視頻上去。我們會看兩個例子:首先,我們把這個效果加到相機(jī)拍攝的影片上去。之后,我們會將這個影響作用于拍攝好的視頻文件。它也可以做到離線渲染,它會把渲染結(jié)果返回給視頻,而不是直接顯示在屏幕上。兩個例子的完整源代碼,請點(diǎn)擊這里

總覽

當(dāng)涉及到處理視頻的時候,性能就會變得非常重要。而且了解黑箱下的原理 —— 也就是 Core Image 是如何工作的 —— 也很重要,這樣我們才能達(dá)到足夠的性能。在 GPU 上面做盡可能多的工作,并且最大限度的減少 GPU 和 CPU 之間的數(shù)據(jù)傳送是非常重要的。之后的例子中,我們將看看這個細(xì)節(jié)。

想對 Core Image 有個初步認(rèn)識的話,可以讀讀 Warren 的這篇文章 Core Image 介紹。我們將使用 Swift 的函數(shù)式 API 中介紹的基于 CIFilter 的 API 封裝。想要了解更多關(guān)于 AVFoundation 的知識,可以看看本期話題中 Adriaan 的文章,還有話題 #21 中的 iOS 上的相機(jī)捕捉

優(yōu)化資源的 OpenGL ES

CPU 和 GPU 都可以運(yùn)行 Core Image,我們將會在 下面 詳細(xì)介紹這兩個的細(xì)節(jié)。在這個例子中,我們要使用 GPU,我們做如下幾樣事情。

我們首先創(chuàng)建一個自定義的 UIView,它允許我們把 Core Image 的結(jié)果直接渲染成 OpenGL。我們可以新建一個 GLKView 并且用一個 EAGLContext 來初始化它。我們需要指定 OpenGL ES 2 作為渲染 API,在這兩個例子中,我們要自己觸發(fā) drawing 事件 (而不是在 -drawRect: 中觸發(fā)),所以在初始化 GLKView 的時候,我們將 enableSetNeedsDisplay 設(shè)置為 false。之后我們有可用新圖像的時候,我們需要主動去調(diào)用 -display。

在這個視圖里,我們保持一個對 CIContext 的引用,它提供一個橋梁來連接我們的 Core Image 對象和 OpenGL 上下文。我們創(chuàng)建一次就可以一直使用它。這個上下文允許 Core Image 在后臺做優(yōu)化,比如緩存和重用紋理之類的資源等。重要的是這個上下文我們一直在重復(fù)使用。

上下文中有一個方法,-drawImage:inRect:fromRect:,作用是繪制出來一個 CIImage。如果你想畫出來一個完整的圖像,最容易的方法是使用圖像的 extent。但是請注意,這可能是無限大的,所以一定要事先裁剪或者提供有限大小的矩形。一個警告:因?yàn)槲覀兲幚淼氖?Core Image,繪制的目標(biāo)以像素為單位,而不是點(diǎn)。由于大部分新的 iOS 設(shè)備配備 Retina 屏幕,我們在繪制的時候需要考慮這一點(diǎn)。如果我們想填充整個視圖,最簡單的辦法是獲取視圖邊界,并且按照屏幕的 scale 來縮放圖片 (Retina 屏幕的 scale 是 2)。

完整的代碼示例在這里:CoreImageView.swift

從相機(jī)獲取像素?cái)?shù)據(jù)

對于 AVFoundation 如何工作的概述,請看 Adriaan 的文章 和 Matteo 的文章 iOS 上的相機(jī)捕捉。對于我們而言,我們想從鏡頭獲得 raw 格式的數(shù)據(jù)。我們可以通過創(chuàng)建一個 AVCaptureDeviceInput 對象來選定一個攝像頭。使用 AVCaptureSession,我們可以把它連接到一個 AVCaptureVideoDataOutput。這個 data output 對象有一個遵守 AVCaptureVideoDataOutputSampleBufferDelegate 協(xié)議的代理對象。這個代理每一幀將接收到一個消息:

func captureOutput(captureOutput: AVCaptureOutput!,
                   didOutputSampleBuffer: CMSampleBuffer!,
                   fromConnection: AVCaptureConnection!) {

我們將用它來驅(qū)動我們的圖像渲染。在我們的示例代碼中,我們已經(jīng)將配置,初始化以及代理對象都打包到了一個叫做 CaptureBufferSource 的簡單接口中去。我們可以使用前置或者后置攝像頭以及一個回調(diào)來初始化它。對于每個樣本緩存區(qū),這個回調(diào)都會被調(diào)用,并且參數(shù)是緩沖區(qū)和對應(yīng)攝像頭的 transform:

source = CaptureBufferSource(position: AVCaptureDevicePosition.Front) {
   (buffer, transform) in
   ...
}

我們需要對相機(jī)返回的數(shù)據(jù)進(jìn)行變換。無論你如何轉(zhuǎn)動 iPhone,相機(jī)的像素?cái)?shù)據(jù)的方向總是相同的。在我們的例子中,我們將 UI 鎖定在豎直方向,我們希望屏幕上顯示的圖像符合照相機(jī)拍攝時的方向,為此我們需要后置攝像頭拍攝出的圖片旋轉(zhuǎn) -π/2。前置攝像頭需要旋轉(zhuǎn) -π/2 并且加一個鏡像效果。我們可以用一個 CGAffineTransform 來表達(dá)這種變換。請注意如果 UI 是不同的方向 (比如橫屏),我們的變換也將是不同的。還要注意,這種變換的代價(jià)其實(shí)是非常小的,因?yàn)樗窃?Core Image 渲染管線中完成的。

接著,要把 CMSampleBuffer 轉(zhuǎn)換成 CIImage,我們首先需要將它轉(zhuǎn)換成一個 CVPixelBuffer。我們可以寫一個方便的初始化方法來為我們做這件事:

extension CIImage {
    convenience init(buffer: CMSampleBuffer) {
        self.init(CVPixelBuffer: CMSampleBufferGetImageBuffer(buffer))
    }
}

現(xiàn)在我們可以用三個步驟來處理我們的圖像。首先,把我們的 CMSampleBuffer 轉(zhuǎn)換成 CIImage,并且應(yīng)用一個形變,使圖像旋轉(zhuǎn)到正確的方向。接下來,我們用一個 CIFilter 濾鏡來得到一個新的 CIImage 輸出。我們使用了 Florian 的文章 提到的創(chuàng)建濾鏡的方式。在這個例子中,我們使用色調(diào)調(diào)整濾鏡,并且傳入一個依賴于時間而變化的調(diào)整角度。最終,我們使用之前定義的 View,通過 CIContext 來渲染 CIImage。這個流程非常簡單,看起來是這樣的:

source = CaptureBufferSource(position: AVCaptureDevicePosition.Front) {
  [unowned self] (buffer, transform) in
    let input = CIImage(buffer: buffer).imageByApplyingTransform(transform)
    let filter = hueAdjust(self.angleForCurrentTime)
    self.coreImageView?.image = filter(input)
}

當(dāng)你運(yùn)行它時,你可能會因?yàn)槿绱说偷?CPU 使用率感到吃驚。這其中的奧秘是 GPU 做了幾乎所有的工作。盡管我們創(chuàng)建了一個 CIImage,應(yīng)用了一個濾鏡,并輸出一個 CIImage,最終輸出的結(jié)果是一個 promise:直到實(shí)際渲染才會去進(jìn)行計(jì)算。一個 CIImage 對象可以是黑箱里的很多東西,它可以是 GPU 算出來的像素?cái)?shù)據(jù),也可以是如何創(chuàng)建像素?cái)?shù)據(jù)的一個說明 (比如使用一個濾鏡生成器),或者它也可以是直接從 OpenGL 紋理中創(chuàng)建出來的圖像。

下面是演示視頻

從影片中獲取像素?cái)?shù)據(jù)

我們可以做的另一件事是通過 Core Image 把這個濾鏡加到一個視頻中。和實(shí)時拍攝不同,我們現(xiàn)在從影片的每一幀中生成像素緩沖區(qū),在這里我們將采用略有不同的方法。對于相機(jī),它會推送每一幀給我們,但是對于已有的影片,我們使用拉取的方式:通過 display link,我們可以向 AVFoundation 請求在某個特定時間的一幀。

display link 對象負(fù)責(zé)在每幀需要繪制的時候給我們發(fā)送消息,這個消息是按照顯示器的刷新頻率同步進(jìn)行發(fā)送的。這通常用來做 自定義動畫,但也可以用來播放和操作視頻。我們要做的第一件事就是創(chuàng)建一個 AVPlayer 和一個視頻輸出:

player = AVPlayer(URL: url)
videoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: pixelBufferDict)
player.currentItem.addOutput(videoOutput)

接下來,我們要創(chuàng)建 display link。方法很簡單,只要創(chuàng)建一個 CADisplayLink 對象,并將其添加到 run loop。

let displayLink = CADisplayLink(target: self, selector: "displayLinkDidRefresh:")
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)

現(xiàn)在,唯一剩下的就是在 displayLinkDidRefresh: 調(diào)用的時候獲取視頻每一幀。首先,我們獲取當(dāng)前的時間,并且將它轉(zhuǎn)換成當(dāng)前播放項(xiàng)目里的時間比。然后我們詢問 videoOutput,如果當(dāng)前時間有一個可用的新的像素緩存區(qū),我們把它復(fù)制一下并且調(diào)用回調(diào)方法:

func displayLinkDidRefresh(link: CADisplayLink) {
    let itemTime = videoOutput.itemTimeForHostTime(CACurrentMediaTime())
    if videoOutput.hasNewPixelBufferForItemTime(itemTime) {
        let pixelBuffer = videoOutput.copyPixelBufferForItemTime(itemTime, itemTimeForDisplay: nil)
        consumer(pixelBuffer)
    }
}

我們從一個視頻輸出獲得的像素緩沖是一個 CVPixelBuffer,我們可以把它直接轉(zhuǎn)換成 CIImage。正如上面的例子,我們會加上一個濾鏡。在這個例子里,我們將組合多個濾鏡:我們使用一個萬花筒的效果,然后用漸變遮罩把原始圖像和過濾圖像相結(jié)合,這個操作是非常輕量級的。

創(chuàng)意地使用濾鏡

大家都知道流行的照片效果。雖然我們可以將這些應(yīng)用到視頻,但 Core Image 還可以做得更多。

Core Image 里所謂的濾鏡有不同的類別。其中一些是傳統(tǒng)的類型,輸入一張圖片并且輸出一張新的圖片。但有些需要兩個 (或者更多) 的輸入圖像并且混合生成一張新的圖像。另外甚至有完全不輸入圖片,而是基于參數(shù)的生成圖像的濾鏡。

通過混合這些不同的類型,我們可以創(chuàng)建意想不到的效果。

混合圖片

在這個例子中,我們使用這些東西:

http://wiki.jikexueyuan.com/project/objc/images/23-5.svg" alt="" />

上面的例子可以將圖像的一個圓形區(qū)域像素化。

它也可以創(chuàng)建交互,我們可以使用觸摸事件來改變所產(chǎn)生的圓的位置。

Core Image Filter Reference 按類別列出了所有可用的濾鏡。請注意,有一部分只能用在 OS X。

生成器和漸變?yōu)V鏡可以不需要輸入就能生成圖像。它們很少自己單獨(dú)使用,但是作為蒙版的時候會非常強(qiáng)大,就像我們例子中的 CIBlendWithMask 那樣。

混合操作和 CIBlendWithAlphaMask 還有 CIBlendWithMask 允許將兩個圖像合并成一個。

CPU vs. GPU

我們在話題 #3 的文章,繪制像素到屏幕上里,介紹了 iOS 和 OS X 的圖形棧。需要注意的是 CPU 和 GPU 的概念,以及兩者之間數(shù)據(jù)的移動方式。

在處理實(shí)時視頻的時候,我們面臨著性能的挑戰(zhàn)。

首先,我們需要能在每一幀的時間內(nèi)處理完所有的圖像數(shù)據(jù)。我們的樣本中采用 24 幀每秒的視頻,這意味著我們有 41 毫秒 (1/24 秒) 的時間來解碼,處理以及渲染每一幀中的百萬像素。

其次,我們需要能夠從 CPU 或者 GPU 上面得到這些數(shù)據(jù)。我們從視頻文件讀取的字節(jié)數(shù)最終會到達(dá) CPU 里。但是這個數(shù)據(jù)還需要移動到 GPU 上,以便在顯示器上可見。

避免轉(zhuǎn)移

一個非常致命的問題是,在渲染管線中,代碼可能會把圖像數(shù)據(jù)在 CPU 和 GPU 之間來回移動好幾次。確保像素?cái)?shù)據(jù)僅在一個方向移動是很重要的,應(yīng)該保證數(shù)據(jù)只從 CPU 移動到 GPU,如果能讓數(shù)據(jù)完全只在 GPU 上那就更好。

如果我們想渲染 24 fps 的視頻,我們有 41 毫秒;如果我們渲染 60 fps 的視頻,我們只有 16 毫秒,如果我們不小心從 GPU 下載了一個像素緩沖到 CPU 里,然后再上傳回 GPU,對于一張全屏的 iPhone 6 圖像來說,我們在每個方向?qū)⒁苿?3.8 MB 的數(shù)據(jù),這將使幀率無法達(dá)標(biāo)。

當(dāng)我們使用 CVPixelBuffer 時,我們希望這樣的流程:

http://wiki.jikexueyuan.com/project/objc/images/23-6.svg" alt="" />

CVPixelBuffer 是基于 CPU 的 (見下文),我們用 CIImage 來包裝它。構(gòu)建濾鏡鏈不會移動任何數(shù)據(jù);它只是建立了一個流程。一旦我們繪制圖像,我們使用了基于 EAGL 上下文的 Core Image 上下文,而這個 EAGL 上下文也是 GLKView 進(jìn)行圖像顯示所使用的上下文。EAGL 上下文是基于 GPU 的。請注意,我們是如何只穿越 GPU-CPU 邊界一次的,這是至關(guān)重要的部分。

工作和目標(biāo)

Core Image 的圖形上下文可以通過兩種方式創(chuàng)建:使用 EAGLContext 的 GPU 上下文,或者是基于 CPU 的上下文。

這個定義了 Core Image 工作的地方,也就是像素?cái)?shù)據(jù)將被處理的地方。與工作區(qū)域無關(guān),基于 GPU 和基于 CPU 的圖形上下文都可以通過執(zhí)行 createCGImage(…),render(_, toBitmap, …)render(_, toCVPixelBuffer, …),以及相關(guān)的命令來向 CPU 進(jìn)行渲染。

重要的是要理解如何在 CPU 和 GPU 之間移動像素?cái)?shù)據(jù),或者是讓數(shù)據(jù)保持在 CPU 或者 GPU 里。將數(shù)據(jù)移過這個邊界是需要很大的代價(jià)的。

緩沖區(qū)和圖像

在我們的例子中,我們使用了幾個不同的緩沖區(qū)圖像。這可能有點(diǎn)混亂。這樣做的原因很簡單,不同的框架對于這些“圖像”有不同的用途。下面有一個快速總覽,以顯示哪些是以基于 CPU 或者基于 GPU 的:

描述
CIImage 它們可以代表兩種東西:圖像數(shù)據(jù)或者生成圖像數(shù)據(jù)的流程。
CIFilter 的輸出非常輕量。它只是如何被創(chuàng)建的描述,并不包含任何實(shí)際的像素?cái)?shù)據(jù)。
如果輸出時圖像數(shù)據(jù)的話,它可能是純像素的 NSData,一個 CGImage, 一個 CVPixelBuffer,或者是一個 OpenGL 紋理
CVImageBuffer 這是 CVPixelBuffer (CPU) 和 CVOpenGLESTexture (GPU) 的抽象父類.
CVPixelBuffer Core Video 像素緩沖 (Pixel Buffer) 是基于 CPU 的。
CMSampleBuffer Core Media 采樣緩沖 (Sample Buffer) 是 CMBlockBuffer 或者 CVImageBuffer 的包裝,也包括了元數(shù)據(jù)。
CMBlockBuffer Core Media 區(qū)塊緩沖 (Block Buffer) 是基于 GPU 的

需要注意的是 CIImage 有很多方便的方法,例如,從 JPEG 數(shù)據(jù)加載圖像或者直接加載一個 UIImage 對象。在后臺,這些將會使用一個基于 CGImageCIImage 來進(jìn)行處理。

結(jié)論

Core Image 是操縱實(shí)時視頻的一大利器。只要你適當(dāng)?shù)呐渲孟?,性能將會是?qiáng)勁的 —— 只要確保 CPU 和 GPU 之間沒有數(shù)據(jù)的轉(zhuǎn)移。創(chuàng)意地使用濾鏡,你可以實(shí)現(xiàn)一些非常炫酷的效果,神馬簡單色調(diào),褐色濾鏡都弱爆啦。所有的這些代碼都很容易抽象出來,深入了解下不同的對象的作用區(qū)域 (GPU 還是 CPU) 可以幫助你提高代碼的性能。

上一篇:Scene Kit下一篇:Swift 方法的多面性