如何實現 iOS 16 帶來的 Depth Effect 圖片效果

語言: CN / TW / HK

本文作者:苯酚

0x01 前言

iOS 16 系統為我們帶來了比較驚豔的桌面鎖屏效果:Depth Effect。它可以使用一張普通圖片當背景,同時可以在適當的地方遮攔住部分桌面組件,形成一種景深的效果(如下圖)。

那麼我們可以在自己的 App 實現類似的效果嗎?一開始我以為 iOS 16 新增了新的的 UIKit 控件,可以像 UIVisualEffectView 一樣幾行簡單的 API 就可以實現,但最後發現沒有。如果給的圖片是已經分好層的多張圖,那麼實現就是簡單的將時鐘控件像夾心餅乾一樣夾在中間即可。然而實踐中發現,網上隨便下載的單張圖片設置為鎖屏背景它也可以達到這種效果。聯想到 iOS 16 的系統相冊在重按後可以將照片中的主體直接分割拖拽出來,於是認為它一定是利用了某些圖像分割算法將前景和背景分離開來,這樣就得到了多層的圖像

0x02 圖像分割(Image Segmentation)

比較經典的圖像分割算法是 分水嶺算法(Watershed),它分割出來的圖像很精準,且邊緣處理非常好,但它要求人工在前景和背景的大概位置上分別畫上一筆(僅一筆就好,後面算法將自動分離出前景和背景),並不適用本文全自動的要求。最近幾年機器學習湧現出了不少的成果,其中之一就是全自動化的圖像分割。果然在經過簡單的搜索後,發現蘋果已經提供預訓練好的模型。

訪問蘋果機器學習官網 https://developer.apple.com/machine-learning/models/ 下載訓練好的模型 DeeplabV3。將模型文件拖到 Xcode 工程中,選中後可以查看它的一些信息:

image

這裏其實我們主要關注模型的輸入、輸出就好,點擊 Predictions 標籤頁,可以看到,模型要求輸入 513x513 的圖片,輸出是成員類型為 Int32,大小 513x513 的二維數組,每個數值表示對應圖像像素點的分類。這裏的成員之所以是 Int32 而不是簡單的 Bool,是因為該模型可以將圖像分割為多個不同的部分,不只是前景和背景。實踐中我們發現,數值為 0 可以認為是背景,非 0 值為前景。

image

下面是一張樣例圖片運行分割之後得到的結果:

image

它被分為了 0 和 15 兩個值,分別就是背景和前景了。

0x03 實踐

模型已經有了,實現方案也差不多了,接下來就是具體的實踐了。

模型拖到 Xcode 工程中後,Xcode 將自動為我們生成一個類:DeepLabV3。我們可以直接創建它的實例而無需任何的 importswift lazy var model = try! DeepLabV3(configuration: { let config = MLModelConfiguration() config.allowLowPrecisionAccumulationOnGPU = true config.computeUnits = .cpuAndNeuralEngine return config }())

然後,用這個實例創建一個 VNCoreMLRequest,請求通過機器學習引擎來分析圖片,並在回調中得到結果: ```swift lazy var request = VNCoreMLRequest(model: try! VNCoreMLModel(for: model.model)) { [unowned self] request, error in if let results = request.results as? [VNCoreMLFeatureValueObservation] { // 最終的分割結果在 arrayValue 中 if let feature = results.first?.featureValue, let arrayValue = feature.multiArrayValue { let width = arrayValue.shape[0].intValue let height = arrayValue.shape[1].intValue let stride = arrayValue.strides[0].intValue // ... }

    }
}

最後在合適的地方創建 `VNImageRequestHandler` 發起請求:swift private func segment() { if let image = self.imageView.image { imageSize = image.size DispatchQueue.global().async { [unowned self] in self.request.imageCropAndScaleOption = .scaleFill let handler = VNImageRequestHandler(cgImage: image.resize(to: .init(width: 513, height: 513)).cgImage!) try? handler.perform([self.request]) } } } ```

注意: 1. request 的回調和 handler 發起請求的代碼在同一個線程中,同步等待結果,所以這裏最好 dispatch 到子線程操作 2. request 需要設置 imageCropAndScaleOption 為 .scallFill,否則它默認將自動裁切中間部分,將得到不符合預期的結果

輸入以下樣例圖片,

將返回的結果 arrayValue 處理成為黑白圖片後的結果:

image

發現它分割的還是挺精準的。當然,如果要在代碼中當掩碼圖(mask)來使用,應當將它處理為背景全透明,而前景不透的圖片:

image

最後,我們將原圖放最下層,其它控件放中間,原圖 + mask 的視圖放最上層,就形成了最終的效果:

image

實際背後的原理就是夾心餅乾:

再多來幾張效果圖:

0x04 後記

當然該模型並不是萬能的,在具體的應用中還存在侷限性,對於有人物的照片分割得較好,但是對於類似大場景的風景照這種可能出現完全無法分割的情況。本文的 Demo 可以在 Github 上找到。

參考資料

  1. https://developer.apple.com/documentation/vision/applying_matte_effects_to_people_in_images_and_video
  2. https://www.appcoda.com.tw/vision-person-segmentation/
  3. https://enlight.nyc/projects/image-segmentation-mobile-app

本文發佈自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe(at)corp.netease.com!