MediaPipe - 跨平臺機器學習應用開發框架

花椒技術發表於2020-03-17

MediaPipe是一款由Google開發並開源的資料流處理機器學習應用開發框架。它是一個基於圖的資料處理管線,用於構建使用了多種形式的資料來源,如視訊、音訊、感測器資料以及任何時間序列資料。 MediaPipe是跨平臺的,可以執行在嵌入式平臺(樹莓派等),移動裝置(iOS和Android),工作站和伺服器上,並支援移動端GPU加速。 使用MediaPipe,可以將機器學習任務構建為一個圖形的模組表示的資料流管道,可以包括推理模型和流媒體處理功能。

為什麼需要MediaPipe

構建包含推理的應用程式所涉及的不僅僅是執行機器學習推理模型。開發者還需要做到以下幾點:

  • 利用各種裝置的功能
  • 平衡裝置資源使用和推理結果的質量
  • 通過流水線並行執行多個操作
  • 確保時間序列資料同步正確

MediaPipe框架解決了這些挑戰,開發者可以使用它輕鬆快速地將現有的或新的機器學習模型組合到以圖表示的原型中,並將其跨平臺實現。開發人員可以配置使用MediaPipe建立的應用程式做到如下幾點:

  • 有效管理資源(CPU和GPU)達到低延遲效能
  • 處理諸如音訊和視訊幀之類的時間序列資料的同步
  • 並測量效能和資源消耗

如在擴增實境(AR)的應用程式中為了增強使用者體驗,程式會以高幀頻處理諸如視訊和音訊之類的感官資料。 由於處理過程的的過度耦合和低延時要求,很難按照常規應用程式開發方式協調資料處理步驟和推理模型。 此外,為不同平臺開發同樣的應用程式也非常耗時,它通常涉及優化推理和處理步驟以便在目標裝置上正確高效地執行。

MediaPipe通過將各個感知模型抽象為模組並將其連線到可維護的圖中來解決這些問題。藉助MediaPipe,可以將資料流處理管道構建為模組化元件圖,包括推理處理模型和媒體處理功能。將視訊和音訊流資料輸入到圖中,通過各個功能模組構建的圖模型管道處理這些資料,如物體檢測或人臉點標註等最後結果資料從圖輸出。

這些功能使開發者可以專注於演算法或模型開發,並使用MediaPipe作為迭代改進其應用程式的環境,其結果可在不同的裝置和平臺上重現。除了上述的特性,MediaPipe還支援TensorFlow和TF Lite的推理引擎,任何TensorFlow和TF Lite的模型都可以在MediaPipe上使用。同時在移動端和嵌入式平臺,MediaPipe也支援裝置本身的GPU加速。

下圖是基於MediaPipe構建的的目標檢測圖:

Object detection

從圖中各個模組的名字可以看出個模組的功能,輸入是攝像頭採集的視訊資料幀通過圖中各個模組的處理輸出到螢幕上。下圖是手機執行效果:

Object detection demo

MediaPipe介紹

MediaPipe有下面三個主要部分組成:

  1. 用於構建基於感官資料進行機器學習推理的框架
  2. 用於效能評估的工具
  3. 可重用的推理和處理元件的集合

基本概念

MediaPipe的核心框架由C++實現,並提供Java以及Objective-C等語言的支援。MediaPipe的主要概念包括:

  • 圖(Graph) - 有向的圖,如上圖所示,資料由Camera送入圖,由Dispaly輸出。
  • 資料包(Packet) - 最基礎的資料單位,一個資料包代表了在某一特定時間節點的資料,上圖中一幀影像為一個資料包。
  • 節點(Node) - 圖中的節點,可以是下面的計算單元或子圖,上圖中每個黃色的矩形是一個節點。
  • 資料流(Stream) - 由按時間順序升序排列的多個資料包組成,資料流的某一特定時間戳只允許至多一個資料包的存在,如採集的連續視訊幀可以看做一個連續資料流,資料流從輸入流經各個節點輸出流出,上圖的黃線就是資料流。
  • 圖配置(GraphConfig) - 描述圖的拓撲和功能的配置資訊,上圖就對應一個配置描述。
  • 計算單元(Calculator) - 按照MeidaPipe協議實現的一個C++類,裡面對資料包進行處理,它作為一個節點,上圖的黃框就是一個計算單元。
  • 子圖(Subgraph) - 也是一個節點,子圖內又包含了一個完整的圖,上圖的黃框可以是一個子圖。

這些概念中主要是圖和計算單元,它們是MediaPipe執行的核心,下面會重點說明它們。

MediaPipe已經包含了多個由Google實現的計算單元,也向使用者提供定製新計算單元的基類。並且子圖的概念是為了方便使用者在多個圖中複用已有的通用元件,例如影像資料的預處理、模型的推理以及影像的渲染等,因此一個MediaPipe圖中的節點既可以是計算單元,亦可以是子圖。子圖在不同圖內的複用,方便了大規模模組化的應用搭建。

可以看出圖是一個有向的資料流管線,一個資料包從資料來源進入,然後按照資料流線路流經各個節點直到輸出結點完成。

圖結構描述通過GraphConfig指定,它通過一個檔案的形式存在可以被Graph載入執行,我們可以通過更新GraphConfig配置檔案來新增,刪除或更改元件的連線。我們還可以在這個檔案裡配置全域性級別設定,以修改圖的執行和資源消耗,這對於調整不同平臺(例如桌上型電腦和移動裝置)上的效能非常有用。

另外在TensorFlow,PyTorch,CNTK或MXNet等專案中使用圖來定義神經網路模型。但MediaPipe的圖起到了補充作用,MediaPipe未定義神經網路的內部結構,而是指定了嵌入一個或多個模型的較大規模的處理圖。

計算單元

計算單元是一個C++類,建立一個計算單元需要使用者繼承於CalculatorBase類並實現GetContract, Open, Process, Close方法去分別定義計算單元的初始化,資料流的處理,以及在計算單元完成所有運算後的關閉步驟。

假設有一個場景,裡面有攝像機,麥克風和光感測器在採集資料並要處理。每個感測器都獨立執行,並且按照各自的取樣率採集資料,由於各個感測器的取樣率不同它們收集併傳送資料就不會同步。假如每個感測器的採集輸出為:

  • 攝像機 - 房間的RGB影像幀(ImageFrame)
  • 麥克風 - 房間中聲音的分貝(整數)
  • 光線感測器 - 房間的亮度(整數)

我們的應用要處理來自這3個感測器的資料,當然不是每個感測器有資料到來就處理,我們要在攝像機的影像幀資料到來時與最後一次收集的麥克風資料和光感測器資料作為一幀資料一起進行處理。這裡我們就需要一個計算單元來完成這個同步工作。在MediaPipe中提供了PacketClonerCalculator計算單元,它在條件滿足時把儲存的最後一幀資料的克隆體作為一幀輸出給下個節點,所以當到達的資料包的時間戳未完全對齊時,這個計算單元可以用來對齊資料包。如下圖所示,它有三個輸入,資料輸入0,資料輸入1和一個觸發輸入(tick),當觸發輸入有值是將最後的兩個資料輸出到輸出埠0和輸出埠1,這樣就達到了資料同步保證了後續節點的資料是完整的。

PacketClonerCalculator

PacketClonerCalculator的完整程式碼:

#include <vector>
#include "absl/strings/str_cat.h"
#include "mediapipe/framework/calculator_framework.h"

namespace mediapipe {

class PacketClonerCalculator : public CalculatorBase {
 public:
  static ::mediapipe::Status GetContract(CalculatorContract* cc) {
    const int tick_signal_index = cc->Inputs().NumEntries() - 1;
    // cc->Inputs().NumEntries() returns the number of input streams
    // for the PacketClonerCalculator
    for (int i = 0; i < tick_signal_index; ++i) {
      cc->Inputs().Index(i).SetAny();
      // cc->Inputs().Index(i) returns the input stream pointer by index
      cc->Outputs().Index(i).SetSameAs(&cc->Inputs().Index(i));
    }
    cc->Inputs().Index(tick_signal_index).SetAny();
    return ::mediapipe::OkStatus();
  }

  ::mediapipe::Status Open(CalculatorContext* cc) final {
    tick_signal_index_ = cc->Inputs().NumEntries() - 1;
    current_.resize(tick_signal_index_);
    // Pass along the header for each stream if present.
    for (int i = 0; i < tick_signal_index_; ++i) {
      if (!cc->Inputs().Index(i).Header().IsEmpty()) {
        cc->Outputs().Index(i).SetHeader(cc->Inputs().Index(i).Header());
        // Sets the output stream of index i header to be the same as
        // the header for the input stream of index i
      }
    }
    return ::mediapipe::OkStatus();
  }

  ::mediapipe::Status Process(CalculatorContext* cc) final {
    // Store input signals.
    for (int i = 0; i < tick_signal_index_; ++i) {
      if (!cc->Inputs().Index(i).Value().IsEmpty()) {
        current_[i] = cc->Inputs().Index(i).Value();
      }
    }

    // Output if the tick signal is non-empty.
    if (!cc->Inputs().Index(tick_signal_index_).Value().IsEmpty()) {
      for (int i = 0; i < tick_signal_index_; ++i) {
        if (!current_[i].IsEmpty()) {
          cc->Outputs().Index(i).AddPacket(
              current_[i].At(cc->InputTimestamp()));
          // Add a packet to output stream of index i a packet from inputstream i
          // with timestamp common to all present inputs
        } else {
          cc->Outputs().Index(i).SetNextTimestampBound(
              cc->InputTimestamp().NextAllowedInStream());
          // if current_[i], 1 packet buffer for input stream i is empty, we will set
          // next allowed timestamp for input stream i to be current timestamp + 1
        }
      }
    }
    return ::mediapipe::OkStatus();
  }

 private:
  std::vector<Packet> current_;
  int tick_signal_index_;
};

REGISTER_CALCULATOR(PacketClonerCalculator);
} 
複製程式碼
  • GetContract() - 定義輸入和輸出資料的型別
  • Open() - 初始化變數
  • Process() - 先儲存輸入資料再判斷是否有觸發資料,有的話就輸出資料,沒有就允許接受下個輸入資料。
  • REGISTER_CALCULATOR - 是在MediaPipe中註冊這個計算單元
  • CalculatorContext - 由MediaPipe Graph提供裡面儲存輸入和輸出資料資訊
  • current_ - 最新的輸入資料

視覺化圖編輯器

MediaPipe提供了MediaPipe Visualizer線上工具,它幫助開發者瞭解其計算單元圖的結構並瞭解其機器學習推理管道的整體行為。這個圖預覽工具允許使用者在編輯器中直接輸入或上傳圖形配置檔案來載入。一個只有視訊剪下計算單元的圖如下所示:

Object detection

可以看到圖顯示在左邊區域它是一個只讀區域,通過滑鼠可以縮放並拖動圖但不能編輯。右邊是文字編輯區可以新增或編輯圖描述程式碼來修改圖,這裡的程式碼就是GraphConfig,它可以被儲存為一個文字檔案然後通過Graph的API來載入這個圖。下面的程式碼是我們又新增一個視訊反轉(Video Flip)的計算單元。更新後的圖如下所示:

input_stream: "input"
output_stream: "output"

node {
  calculator: "VideoClipCalculator"
  input_stream: "IN:input"
  output_stream: "clippedVideoOutput"
}

node {
  calculator: "VideoFlipCalculator"
  input_stream: "clippedVideoOutput"
  output_stream: "OUT:output"
}
複製程式碼

Object detection

圖配置程式碼簡單說明如下:

  • input_stream - 輸入流名字
  • output_stream - 輸出流名字
  • node - 定義節點
    • calculator - 節點的計算單元類名
    • input_stream - 節點的輸入流名字
    • output_stream - 節點的輸出流名字

可以看到VideoClipCalculator節點使用input作為輸入,然後輸出clippedVideoOutput,VideoFlipCalculator節點使用clippedVideoOutput作為輸入,最後輸出output。另外圖配置還有另外一些引數配置和命名規則這裡就不再說了。

目前基於MediaPipe實現的示例

下面這些都是Google利用MediaPipe框架實現的移動端應用示例,當然整個基於MediaPipe的開源專案還有桌面應用示例,瀏覽器應用示例和Google Coral應用示例。

  • 物體檢測(Object Detection)
  • 物體檢測並追蹤(Object Detection and Tracking)
  • 人臉檢測(Face Detection)
  • 單手檢測(Hand Detection)
  • 單手追蹤(Hand Tracking)
  • 多手追蹤(Multi-hand Tracking)
  • 頭髮分割(Hair Segmentation)

物體檢測(Object Detection)的圖

下圖就是物體檢測的MeidaPipe圖,可以看出從上面的視訊輸入到下面的視訊輸出整個過程還是有不少計算單元的,其中僅TfLiteInference計算單元基於TensorFlow Lite完成推理。

Object detection

我們從上而下說明一下每個計算單元的作用:

  • input_video - 輸入視訊
  • FlowLimiter - 資料限流計算單元,它會接收下面計算單元的一個輸入訊號,如黃色虛線所示,如果沒有下面單元的輸入訊號它會丟棄當前的視訊幀,這樣就可以控制處理過程不會因為輸人間隔小於處理時間而出現問題。
  • TFLiteConverter - 將輸入圖片轉化成TF Lite模型可處理的張量
  • TFLiteInference - TF Lite模型推理
  • SsdAnchors - 生成用於解碼模型的Anchors
  • TFLiteTensorsToDetections - 將模型的輸出轉化成偵測結果
  • NoMaxSuppression - non-maximum suppression演算法為了去除重複的物體
  • DetectionLabelIdToText - 將檢測結果轉化成對應的物體名稱
  • DetectionsToRenderData - 將檢測的結果的資料轉化成渲染資料
  • AnnotationOverlay - 標註資料疊加到當前視訊幀,它需要從FlowLimiter的原始視訊幀
  • Output_video - 最終輸出的視訊幀

總結

MediaPipe裡還有邊資料包(Side packets), 輸入策略(Input policies),執行時行為(Runtime behavior)等等概念就不再說明了,有興趣可以看官方文件。

可以說是MediaPipe是一個利用“有序管線”圖的應用程式開發框架,甚至可以基於它開發一個完全沒有機器學習推理的應用程式,但是由於它基於圖的這樣一個架構使其很適合開發含有推理模型的應用。

MediaPipe用Bazel構建工具來構建應用,庫和測試工具,MediaPipe框架及裡面的所有示例包括iOS端的都是用這個工具構建的,所有要會使用這個跨平臺構建工具。

參考

MediaPipe文件
mediapipe.readthedocs.io/en/latest/

相關文章