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

雲音樂技術團隊發表於2023-02-13
本文作者:苯酚

0x01 前言

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

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

0x02 影像分割(Image Segmentation)

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

訪問蘋果機器學習官網 https://developer.apple.com/m... 下載訓練好的模型 DeeplabV3。將模型檔案拖到 Xcode 工程中,選中後可以檢視它的一些資訊:

image

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

image

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

image

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

0x03 實踐

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

模型拖到 Xcode 工程中後,Xcode 將自動為我們生成一個類:DeepLabV3。我們可以直接建立它的例項而無需任何的 import

    lazy var model = try! DeepLabV3(configuration: {
        let config = MLModelConfiguration()
        config.allowLowPrecisionAccumulationOnGPU = true
        config.computeUnits = .cpuAndNeuralEngine
        return config
    }())

然後,用這個例項建立一個 VNCoreMLRequest,請求透過機器學習引擎來分析圖片,並在回撥中得到結果:

    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 發起請求:

    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/d...
  2. https://www.appcoda.com.tw/vi...
  3. https://enlight.nyc/projects/...
本文釋出自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe(at)corp.netease.com!

相關文章