作者:Lucca,iOS 初學者,目前就職於字節跳動抖音iOS業務安全組。
Sessions:
https://developer.apple.com/videos/play/wwdc2020/10008/
https://developer.apple.com/videos/play/wwdc2020/10021/https://developer.apple.com/videos/play/wwdc2020/10089/
同時感謝 @Puttin 和 @jojotov 的幫助
概覽本文含括了本次WWDC2020裡Core Image專題三個文章的脫水翻譯。分別是:
優化視頻處理終端Core Image工作流(10008-Optimize the Core Image pipeline for your video app[1])如何構建基於Metal的Core Image內核(10021-Build Metal-based Core Image kernels with Xcode[2])探索Core Image的debug技巧(10089-Discover Core Image debugging techniques[3])這幾篇Session主要講的是對於CoreImage處理視頻濾鏡時的一些最佳實踐,同時也簡單介紹了怎麼更好的去debug整個Core Image的渲染流程。基於文章內容以及筆者自己的一些理解,將文章的整體大綱定義如下:
優化視頻處理終端Core Image工作流創建 CIContext 的建議每個視圖只創建一個 CIContextContext 的創建比較容易,但初始化需要花費較多的時間和內存,因此我們需要儘量減少重複創建 CIContext 所帶來的性能損耗。
通過 CIContextOption 創建 CIContext創建 CIContext 時可以傳入需要的 CIContextOption(更多信息請參考 蘋果官方文檔[4]),其中有一些選項可以幫助我們優化所創建的 CIContext:
.cacheIntermediates:在渲染過程中,是否需要緩存中間像素緩衝區的內容。由於我們面對的是視頻處理,而在視頻中,絕大部分情況下每一幀的內容都與上一幀不同,因此關閉緩存可以非常有效地減少內存佔用。把這個選項設置為 false 對我們的優化非常重要!
.name:為 context 設置一個名字可以幫助我們調試 Core Image,更多詳細內容可以參考 [CoreImage Debugging Techniques]( "CoreImage Debugging Techniques")。
結合使用 Metal 和 CIContext在有些時候,我們可能需要混合使用 Core Image 和其他的 Metal 特性,例如我們 Core Image 的輸入或者輸出是 MTLTexture時。在這種情況下,session 中提到一種推薦的處理方法:使用 MTLCommandQueue 來構造 CIContext 實例。
為了解釋為何需要通過這種方式構造 CIContext,我們首先考慮一下此情況下的流程時間線,假設我們的應用使用一條 MTLCommandQueue 隊列來渲染一個 Metal 紋理,此時 CPU 和 GPU 都會進行相應的工作:
接下來,我們把渲染好的 Metal 紋理傳入 Core Image,假如此時 Core Image 沒有顯示地設置隊列,它會使用內置的獨立 Metal 隊列進行工作:
最後,Core Image 處理完成後的紋理對象會再次在一開始的 Metal 隊列中進行其他處理:
可以看到,由於 CoreImage 和 Metal 的工作都在不同隊列中進行,在不同的工作切換時,應用必須發出等待的指令來保證接收到正確的結果,這造成了不必要的性能和時間損耗:
為了解決這個問題,消除等待造成的浪費,我們可以讓 Core Image 和 Metal 公用同一條 MTLCommandQueue 隊列,這樣的話 Metal 的渲染工作和 Core Image 處理工作之間就不會有等待的間隙,整個流程會變得更加高效:
蘋果文檔中關於其他 Core Image 性能相關的最佳實踐請參考 Getting the Best Performance[5]
儘量使用內置的 CIFilter為了獲得更好的性能,使用內置的 CIFilter 是最簡單有效的方法:
Built-in 的 CIFilters 為 Metal 單獨作了優化,目前所有的內置 CIFilter 都是使用 Metal 實現的
步驟:
使用 Built-in 的 CIFilter 添加模糊濾鏡:
import CoreImage.CIFilterBuiltins
func motionBlur(inputImage: CIImage) -> CIImage? {
let motionBlurFilter = CIFilter.motionBlur()
motionBlurFilter.inputImage = inputImage
motionBlurFilter.angle = 0
motionBlurFilter.radius = 20
return motionBlurFilter.outputImage
}更多關於使用 Core Image 內置濾鏡的信息,請參考蘋果官方文檔 Processing an Image Using Built-in Filters[6]。
編寫並應用自定義的 CI Kernels為什麼要使用自定義的CI Kernels提升語言的運行性能(例如聚集讀取、組寫入、half-float的數學計算)
在應用程式中加入基於Metal的自定義Core Image內核這裡只需要簡單的五步操作就可以把自定義Core Image內核加到你的應用中
添加自定義的構建規則到你的工程中首先我們針對.ci.metal結尾的文件添加構建規則,對於所有以此結尾的文件我們都將調用如下腳本,該腳本的-fcikernel標誌表示此類文件會使用metal編譯器構建出一個.ci.air結尾的二進位文件。
其次我們針對.ci.air的文件也添加一條構建規則,該規則運行的腳本的-cikernel標誌會調用metal的連結程序,最後在應用程式的目錄中產出一個.ci.mentallib結尾的文件。
添加.ci.metal結尾的源文件到你的工程中通過文件-新建,我們可以新建一個Meta File類型的文件,然後命名需要以.ci結尾,以便最後產出的文件是以.ci.metal結尾
<<< 左右滑動見更多 >>>
編寫Metal內核本次使用的內核是在Session*[Edit and Playback HDR video with AVFoundation]( "Edit and Playback HDR video with AVFoundation")*中使用的內核
在這個 Demo 中我們實現了一種斑馬條紋的效果,可以突出顯示HDR視頻的明亮部分、擴展其中的中心區域。
下面我們通過代碼來演示如何通過自定義內核實現這個效果:
定義這個效果的函數,這個函數必須標識extern "C"(表示這個函數會使用C語言編譯)
// MyKernels.ci.metal
#include <CoreImage/CoreImage.h> // includes CIKernelMetalLib.h
using namespace metal;
extern "C" float4 HDRZebra (coreimage::sample_t s, float time, coreimage::destination dest)
{
float diagLine = dest.coord().x + dest.coord().y;
float zebra = fract(diagLine/20.0 + time*2.0);
if ((zebra > 0.5) && (s.r > 1 || s.g > 1 || s.b > 1))
return float4(2.0, 0.0, 0.0, 1.0);
return s;
}因為這是一個CIColorKernel,所以返回值必須是float4,這裡接受的第一個參數是sample_t_s,表示輸入圖片的像素。最後一個參數是一個提供返回像素的坐標(destination)的一個結構體。在代碼實現中,我們根據dest來確定處於哪個對角線,然後通過一些簡單的計算判斷是否處於斑馬紋上,且這個像素的亮度超過了正常亮度標準,我們就返回一個亮紅色的像素。其他情況就返回原像素。
最終實現的效果就是如下這樣:
關於更多Core Image內核中Metal Shader Language的知識,你可以訪問我們的官方網站來下載更多相關內容。Metal Shader Language For Core Image Kernels[7]
使用 Swift 代碼加載內核並生成一張圖片通過以下代碼我們就能加載內核並用它來創建圖片
class HDRZebraFilter: CIFilter {
var inputImage: CIImage?
var inputTime: Float = 0.0
static var kernel: CIColorKernel = { () -> CIColorKernel in
let url = Bundle.main.url(forResource: "MyKernels",
withExtension: "ci.metallib")!
let data = try! Data(contentsOf: url)
return try! CIColorKernel(functionName: "HDRzebra",
fromMetalLibraryData: data)
}()
override var outputImage : CIImage? {
get {
guard let input = inputImage else {return nil}
return HDRZebraFilter.kernel.apply(extent: input.extent,
arguments: [input, inputTime])
}
}
}內核經常被用於子類化CIFilter,他一般會接受一個inputImage的CIImage對象和一些其他參數。我們建議將kernel定義為靜態變量,這樣我們就只需要在第一次使用到這個內核對象的時候去加載metallib資源。
接下來我們需要重寫outputImage這個變量,在這個變量的getter方法裡,我們可以拿到靜態變量加載好的內核,然後通過編寫好的斑馬紋函數來生成一張新的圖片!
如何更好地渲染到視圖務必避免使用 UIImageView 和 NSImageView,他們是為靜態圖片而設計的
最簡單的選擇:AVPlayerView:自動播放視頻、
使用AVPlayerView使用AVPlayerView是非常方便的,代碼如下:
let videoComposition = AVMutableVideoComposition(
asset: asset,
applyingCIFiltersWithHandler:
{ (request: AVAsynchronousCIImageFilteringRequest) -> Void in
let filter = HDRZebraFilter()
filter.inputImage = request.sourceImage
let output = filter.outputImage
if (output != nil) {
request.finish(with: output, context: myCtx)
}
else { request.finish(with: err) }
}
)關鍵的對象是AVMutableVideoComposition,他通過一個video asset和一個block初始化。這個block傳遞過來一個AVAsynchronousCIImageFilteringRequest的request對象。block會在視頻的每一幀被調用,你所需要做的就是創建一個CIFilter,設置好輸入圖像(也就是當前視頻幀的圖片request.sourceImage),拿到濾鏡渲染好的圖像傳遞給request對象就大功告成了。
同時你也可以通過debug過程的快速預覽功能了解到更多渲染過程中的信息
比如你可以看到輸入的視頻幀是一個10位的HDR圖形。關於更多的debug技巧我們會在下文深入了解
使用MTKView第一步你需要做的是重寫MTKView的init方法
class MyView : MTKView {
var context: CIContext
var commandQueue : MTLCommandQueue
override init(frame frameRect: CGRect, device: MTLDevice?) {
let dev = device ?? MTLCreateSystemDefaultDevice()!
context = CIContext(mtlDevice: dev, options: [.cacheIntermediates : false] )
commandQueue = dev.makeCommandQueue()!
super.init(frame: frameRect, device: dev)
framebufferOnly = false // allow Core Image to use Metal Compute
colorPixelFormat = MTLPixelFormat.rgba16Float
if let caml = layer as? CAMetalLayer {
caml.wantsExtendedDynamicRangeContent = true
}
}在init過程中是我們創建CIContext的最佳過程,確保設置.cacheIntermediates為false,這樣Core Image才能使用到Metal compute。在macOS中如果你的視圖需要支持HDR,你需要設置colorPixelFormat為rgba16Float,同時設置caml.wantsExtendedDynamicRangeContent為true。接下來我們需要重寫draw方法
func draw(in view: MTKView) {
let size = self.convertToBacking(self.bounds.size)
let rd = CIRenderDestination(width: Int(size.width),
height: Int(size.height),
pixelFormat: colorPixelFormat,
commandBuffer: nil)
{ () -> MTLTexture in return view.currentDrawable!.texture }
context.startTask(toRender:image, from:rect, to:rd, at:point)
// Present the current drawable
let cmdBuf = commandQueue.makeCommandBuffer()!
cmdBuf.present(view.currentDrawable!)
cmdBuf.commit()
}在draw方法中我們需要通過特殊的方法創建CIRenderDestination對象,我們通過正確的寬高和像素格式來創建destination,但是過程中我們不是直接返回Metal紋理(texture),而是通過一個block的形式返回紋理。這個可以讓CIContext在上一幀完成前就可以把Metal的任務入隊,接下來我們告訴CIContext可以開始任務。最後一步就是創建一個命令緩衝區去將當前繪製的圖像呈現到視圖上。
探索更強大的 Core Image Debug 技巧CI_PRINT_TREE 是什麼CI_PRINT_TREE基於的原理和為XCode提供Core Image預覽功能的原理是一樣的。例如對於下面代碼,如果我們把滑鼠聚焦在output上,會有一個浮窗展示這個變量的對象地址,如果再點擊上面的眼睛圖標,就能看到一個可視化的界面展示產生這個圖片過程中的一些方法調用以及一些過程信息。
而圖片的快速預覽功能只是CI_PRINT_TREE的一個基礎應用,CI_PRINT_TREE是一個非常靈活的環境變量,他可以設置多種的模式和操作,讓你可以了解到Core Image是如何優化和渲染圖片的。
如何開啟和控制CI_TREE_PRINT開啟CI_PRINT_TREE在啟動APP之前通過終端開啟CI_PRINT_TREE的環境變量控制CI_PRINT_TREECI_PRINT_TREE主要接收三個參數:graph type,output type以及options
graph type表示了Core Image渲染過程的三個階段:
1表示初始化階段,這個階段對於了解本次渲染使用了什麼顏色空間是很有幫助的。
2表示圖像優化階段,可以看到core image的優化過程。
4表示Core Image把圖像連結到GPU中的過程,這對於了解渲染過程中使用了多少個中間緩衝區很有幫助。
我們可以通過組合的方式同時列印上述幾個階段。例如:7表示3個階段都列印,3表示初始化和優化階段。
<<< 左右滑動見更多 >>>
output type用來指定生成文檔的輸出類型。可以指定為pdf和png,最終輸出的結果將會保存在macOS的緩存路徑以及iOS的文檔目錄(Documents)。如果未指定輸出類型,那文本將會以標準格式的壓縮文本形式輸出。同時你可以通過指定CI_LOG_FILE =「 oslog」將文本信息輸出到Console.app裡,在OS開發調試中這樣會更加的方便。
options可以指定很多選項來更精確的輸出信息。例如:
指定context==name,可以只輸出特定名字上下文的相關信息。
frame-n,可以只記錄每個上下文的第n次的渲染過程。
剩下幾個選項可以將輸入圖像中間圖像以及輸出圖像都輸出到文檔中,這會提供很多有用的信息,但同時也會生成文檔的時間和內存,確保在你需要這些信息的時候選擇這些參數。
如何獲取和理解CI_PRINT_TREE文件如何獲取CI_PRINT_TREE文件在macOS中這將會非常容易,只要定位到緩存文件目錄就可以看到生成的CI_PRINT_TREE文檔:
<<< 左右滑動見更多 >>>
在iOS中首先要確保info.plist中的Application supports iTunes file sharing的值是YES
然後連接手機到電腦,在Finder側邊欄找到你的設備並切換到files的窗口,這裡就可以看到所有的CI_PRINT_TREE文件,然後複製到macOS中就可以了
如何理解CI_PRINT_TREE文件首先輸出在底部,輸入在頂部。綠色的節點是裝飾內核(wrap kernels),紅色的節點是色彩內核。
在初始化的樹種尋找顏色空間是很方便的,可以看到這裡是HLG的顏色空間
同時每個節點會顯示他的ROI,表示「region of interest」,意思是這次渲染過程中每個節點需要的區域大小
如果指定了graph type為4,且options為dunp-intermediates。文檔會輸出除了輸出通道以外的所有中間緩衝區,這對於定位渲染錯誤是如何發生是很有幫助的。
同時可以看到每個過程中各個中間緩衝區的執行時間、像素數量和像素格式,這對於理解哪個過程消耗了更多的內存和時間是很有幫助的。
<<< 左右滑動見更多 >>>
結語本次Core Image的專題講的東西其實不是很高深,但也需要一定的Core Image基礎才能夠更好的了解。而且其中對於很多知識點的描述都是比較簡略的,如果大家對於這塊感興趣還是要去更詳細的閱讀CoreImage的官方專題。Core Image[8]
推薦閱讀#Metal
關注我們我們是「老司機技術周報」,每周會發布一份關於 iOS 的周報,也會定期分享一些和 iOS 相關的技術。歡迎關注。
關注有禮,關注【老司機技術周報】,回復「2020」,領取學習大禮包。
支持作者這篇文章的內容來自於 《WWDC20 內參》。在這裡給大家推薦一下這個專欄,專欄目前已經創作了 108 篇文章,只需要 29.9 元。點擊【閱讀原文】,就可以購買繼續閱讀 ~
WWDC 內參 系列是由老司機周報、知識小集合以及 SwiftGG 幾個技術組織發起的。已經做了幾年了,口碑一直不錯。 主要是針對每年的 WWDC 的內容,做一次精選,並號召一群一線網際網路的 iOS 開發者,結合自己的實際開發經驗、蘋果文檔和視頻內容做二次創作。
參考資料[1]Optimize the Core Image pipeline for your video app: https://developer.apple.com/videos/play/wwdc2020/10008/
[2]Build Metal-based Core Image kernels with Xcode: https://developer.apple.com/videos/play/wwdc2020/10021/
[3]Discover Core Image debugging techniques: https://developer.apple.com/videos/play/wwdc2020/10089/
[4]蘋果官方文檔: https://developer.apple.com/documentation/coreimage/cicontextoption
[5]Getting the Best Performance: https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/CoreImaging/ci_performance/ci_performance.html#//apple_ref/doc/uid/TP30001185-CH10-SW1
[6]Processing an Image Using Built-in Filters: https://developer.apple.com/documentation/coreimage/processing_an_image_using_built-in_filters
[7]Metal Shader Language For Core Image Kernels: https://developer.apple.com/metal/MetalCIKLReference6.pdf
[8]Core Image: https://developer.apple.com/documentation/coreimage