Máy tính

Mỗi công cụ tính là một nút của biểu đồ. Chúng tôi mô tả cách tạo một máy tính mới, cách khởi chạy máy tính, cách thực hiện các phép tính, luồng đầu vào và đầu ra, dấu thời gian và các tuỳ chọn. Mỗi nút trong biểu đồ được triển khai dưới dạng Calculator. Phần lớn quá trình thực thi biểu đồ diễn ra bên trong các máy tính. Một máy tính có thể không nhận hoặc có nhiều luồng đầu vào và/hoặc gói phụ, đồng thời không tạo ra hoặc có nhiều luồng đầu ra và/hoặc gói phụ.

CalculatorBase

Trình tính toán được tạo bằng cách xác định một lớp con mới của lớp CalculatorBase, triển khai một số phương thức và đăng ký lớp con mới bằng Mediapipe. Ở mức tối thiểu, một máy tính mới phải triển khai bốn phương pháp dưới đây

  • GetContract()
    • Tác giả máy tính có thể chỉ định các loại dữ liệu đầu vào và đầu ra dự kiến của một máy tính trong GetContract(). Khi một biểu đồ được khởi chạy, khung này sẽ gọi một phương thức tĩnh để xác minh xem các loại gói của dữ liệu đầu vào và đầu ra được kết nối có khớp với thông tin trong quy cách này hay không.
  • Open()
    • Sau khi biểu đồ bắt đầu, khung này sẽ gọi Open(). Tại thời điểm này, máy tính đã có thể sử dụng các gói phía đầu vào. Open() diễn giải các thao tác cấu hình nút (xem Biểu đồ) và chuẩn bị trạng thái của máy tính trên mỗi lần chạy biểu đồ. Hàm này cũng có thể ghi các gói vào kết quả của máy tính. Lỗi trong lúc Open() có thể chấm dứt quá trình chạy biểu đồ.
  • Process()
    • Đối với một máy tính có dữ liệu đầu vào, khung sẽ gọi Process() nhiều lần bất cứ khi nào có ít nhất một luồng đầu vào có sẵn một gói. Theo mặc định, khung này đảm bảo rằng tất cả các dữ liệu đầu vào đều có cùng dấu thời gian (xem phần Đồng bộ hoá để biết thêm thông tin). Hệ thống có thể gọi đồng thời nhiều lệnh gọi Process() khi bật tính năng thực thi song song. Nếu xảy ra lỗi trong Process(), khung sẽ gọi Close() và quá trình chạy biểu đồ sẽ chấm dứt.
  • Close()
    • Sau khi tất cả các lệnh gọi đến Process() kết thúc hoặc khi tất cả luồng đầu vào đóng, khung sẽ gọi Close(). Hàm này luôn được gọi nếu Open() được gọi và thành công, cũng như ngay cả khi biểu đồ chạy bị chấm dứt do lỗi. Không có dữ liệu đầu vào qua bất kỳ luồng đầu vào nào trong Close(), nhưng vẫn có quyền truy cập vào các gói phía đầu vào, do đó có thể ghi đầu ra. Sau khi trả về Close(), máy tính sẽ được coi là một nút chết. Đối tượng máy tính bị huỷ ngay khi biểu đồ chạy xong.

Sau đây là các đoạn mã lấy từ CalculatorBase.h.

class CalculatorBase {
 public:
  ...

  // The subclasses of CalculatorBase must implement GetContract.
  // ...
  static absl::Status GetContract(CalculatorContract* cc);

  // Open is called before any Process() calls, on a freshly constructed
  // calculator.  Subclasses may override this method to perform necessary
  // setup, and possibly output Packets and/or set output streams' headers.
  // ...
  virtual absl::Status Open(CalculatorContext* cc) {
    return absl::OkStatus();
  }

  // Processes the incoming inputs. May call the methods on cc to access
  // inputs and produce outputs.
  // ...
  virtual absl::Status Process(CalculatorContext* cc) = 0;

  // Is called if Open() was called and succeeded.  Is called either
  // immediately after processing is complete or after a graph run has ended
  // (if an error occurred in the graph).  ...
  virtual absl::Status Close(CalculatorContext* cc) {
    return absl::OkStatus();
  }

  ...
};

Vòng đời của một chiếc máy tính

Trong quá trình khởi chạy biểu đồ MediaPipe, khung này sẽ gọi một phương thức tĩnh GetContract() để xác định các loại gói dự kiến.

Khung này sẽ tạo và huỷ toàn bộ máy tính cho mỗi lần chạy biểu đồ (ví dụ: một lần cho mỗi video hoặc một lần cho mỗi hình ảnh). Bạn nên cung cấp các đối tượng lớn hoặc tốn kém nhưng không đổi qua các lần chạy biểu đồ dưới dạng các gói phía đầu vào để không lặp lại các phép tính trong những lần chạy tiếp theo.

Sau khi khởi chạy, trình tự sau đây sẽ xảy ra đối với mỗi lần chạy biểu đồ:

  • Open()
  • Process() (lặp lại)
  • Close()

Khung này gọi Open() để khởi chạy công cụ tính toán. Open() sẽ diễn giải mọi tuỳ chọn và thiết lập trạng thái chạy trên mỗi biểu đồ của máy tính. Open() có thể lấy các gói phía đầu vào và ghi các gói vào đầu ra của máy tính. Nếu thích hợp, ứng dụng sẽ gọi SetOffset() để giảm tình trạng lưu gói vào bộ đệm có thể xảy ra với luồng đầu vào.

Nếu xảy ra lỗi trong Open() hoặc Process() (như được biểu thị bằng một trong số các biến này trả về trạng thái không phải Ok), thì quá trình chạy biểu đồ sẽ bị chấm dứt mà không có lệnh gọi nào khác đến các phương thức của trình tính toán và máy tính sẽ bị huỷ.

Đối với một máy tính có dữ liệu đầu vào, khung sẽ gọi Process() bất cứ khi nào có sẵn ít nhất một dữ liệu đầu vào. Khung này đảm bảo rằng các dữ liệu đầu vào đều có cùng dấu thời gian, dấu thời gian đó sẽ tăng lên khi có lệnh gọi đến Process() và tất cả các gói đều được phân phối. Do đó, một số dữ liệu đầu vào có thể không có gói nào khi Process() được gọi. Một dữ liệu đầu vào có gói bị thiếu dường như sẽ tạo ra một gói trống (không có dấu thời gian).

Khung này gọi Close() sau tất cả các lệnh gọi đến Process(). Mọi đầu vào sẽ đã hết, nhưng Close() có quyền truy cập vào các gói phía đầu vào và có thể ghi đầu ra. Sau khi trả về lệnh Close, máy tính sẽ bị huỷ.

Máy tính không có dữ liệu đầu vào được gọi là nguồn. Trình tính toán nguồn sẽ tiếp tục gọi Process() miễn là nó trả về trạng thái Ok. Công cụ tính nguồn cho biết nguồn đã hết bằng cách trả về một trạng thái dừng (chẳng hạn như mediaPipe::tool::StatusStop().).

Xác định đầu vào và đầu ra

Giao diện công khai đối với một máy tính bao gồm một tập hợp các luồng đầu vào và luồng đầu ra. Trong CalculatorGraphConfiguration, kết quả đầu ra từ một số máy tính được kết nối với dữ liệu đầu vào của các máy tính khác bằng cách sử dụng các luồng được đặt tên. Tên luồng thường là chữ thường, trong khi thẻ đầu vào và thẻ đầu ra thường là CHỮ HOA. Trong ví dụ bên dưới, dữ liệu đầu ra có tên thẻ VIDEO được kết nối với dữ liệu đầu vào có tên thẻ VIDEO_IN thông qua luồng có tên video_stream.

# Graph describing calculator SomeAudioVideoCalculator
node {
  calculator: "SomeAudioVideoCalculator"
  input_stream: "INPUT:combined_input"
  output_stream: "VIDEO:video_stream"
}
node {
  calculator: "SomeVideoCalculator"
  input_stream: "VIDEO_IN:video_stream"
  output_stream: "VIDEO_OUT:processed_video"
}

Bạn có thể xác định luồng đầu vào và đầu ra theo số chỉ mục, theo tên thẻ hoặc bằng tổ hợp tên thẻ và số chỉ mục. Bạn có thể xem một số ví dụ về giá trị nhận dạng đầu vào và đầu ra trong ví dụ bên dưới. SomeAudioVideoCalculator xác định đầu ra video theo thẻ và đầu ra âm thanh bằng tổ hợp thẻ và chỉ mục. Đầu vào có thẻ VIDEO đã được kết nối với luồng có tên video_stream. Đầu ra có thẻ AUDIO, chỉ mục 01 được kết nối với các luồng có tên audio_leftaudio_right. SomeAudioCalculator chỉ xác định đầu vào âm thanh theo chỉ mục (không cần thẻ).

# Graph describing calculator SomeAudioVideoCalculator
node {
  calculator: "SomeAudioVideoCalculator"
  input_stream: "combined_input"
  output_stream: "VIDEO:video_stream"
  output_stream: "AUDIO:0:audio_left"
  output_stream: "AUDIO:1:audio_right"
}

node {
  calculator: "SomeAudioCalculator"
  input_stream: "audio_left"
  input_stream: "audio_right"
  output_stream: "audio_energy"
}

Trong quá trình triển khai công cụ tính, dữ liệu đầu vào và đầu ra cũng được xác định theo tên thẻ và số chỉ mục. Trong hàm dưới đây, đầu vào và đầu ra được xác định:

  • Theo số chỉ mục: Luồng đầu vào kết hợp được xác định bằng chỉ mục 0.
  • Theo tên thẻ: Luồng đầu ra video được xác định theo tên thẻ "VIDEO".
  • Theo tên thẻ và số chỉ mục: Luồng âm thanh đầu ra được xác định bằng tổ hợp tên thẻ AUDIO với số chỉ mục 01.
// c++ Code snippet describing the SomeAudioVideoCalculator GetContract() method
class SomeAudioVideoCalculator : public CalculatorBase {
 public:
  static absl::Status GetContract(CalculatorContract* cc) {
    cc->Inputs().Index(0).SetAny();
    // SetAny() is used to specify that whatever the type of the
    // stream is, it's acceptable.  This does not mean that any
    // packet is acceptable.  Packets in the stream still have a
    // particular type.  SetAny() has the same effect as explicitly
    // setting the type to be the stream's type.
    cc->Outputs().Tag("VIDEO").Set<ImageFrame>();
    cc->Outputs().Get("AUDIO", 0).Set<Matrix>();
    cc->Outputs().Get("AUDIO", 1).Set<Matrix>();
    return absl::OkStatus();
  }

Đang xử lý

Process() được gọi trên một nút không phải nguồn phải trả về absl::OkStatus() để cho biết rằng mọi việc đều tốt hoặc bất kỳ mã trạng thái nào khác để báo hiệu lỗi

Nếu một công cụ tính không phải nguồn trả về tool::StatusStop(), thì tức là biểu đồ này đang bị huỷ sớm. Trong trường hợp này, tất cả các máy tính nguồn và luồng đầu vào biểu đồ sẽ bị đóng (và các Gói còn lại sẽ truyền qua biểu đồ).

Một nút nguồn trong biểu đồ sẽ tiếp tục được gọi Process() miễn là nút đó trả về absl::OkStatus(). Để cho biết rằng không có dữ liệu nào được tạo, hãy trả về tool::StatusStop(). Bất kỳ trạng thái nào khác cho biết đã xảy ra lỗi.

Close() trả về absl::OkStatus() để chỉ báo đã thành công. Bất kỳ trạng thái nào khác cho biết không thành công.

Dưới đây là hàm Process() cơ bản. Phương thức này sử dụng phương thức Input() (chỉ có thể sử dụng nếu máy tính có một dữ liệu đầu vào) để yêu cầu dữ liệu đầu vào. Sau đó, phương thức này sẽ sử dụng std::unique_ptr để phân bổ bộ nhớ cần thiết cho gói đầu ra và thực hiện các phép tính. Sau khi hoàn tất, thao tác này sẽ giải phóng con trỏ khi thêm vào luồng đầu ra.

absl::Status MyCalculator::Process() {
  const Matrix& input = Input()->Get<Matrix>();
  std::unique_ptr<Matrix> output(new Matrix(input.rows(), input.cols()));
  // do your magic here....
  //    output->row(n) =  ...
  Output()->Add(output.release(), InputTimestamp());
  return absl::OkStatus();
}

Tuỳ chọn máy tính

Máy tính chấp nhận các tham số xử lý thông qua (1) gói luồng đầu vào (2) gói phía đầu vào và (3) các tuỳ chọn của công cụ tính toán. Các tuỳ chọn máy tính, nếu được chỉ định, sẽ xuất hiện dưới dạng giá trị cố định trong trường node_options của thông báo CalculatorGraphConfiguration.Node.

  node {
    calculator: "TfLiteInferenceCalculator"
    input_stream: "TENSORS:main_model_input"
    output_stream: "TENSORS:main_model_output"
    node_options: {
      [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] {
        model_path: "mediapipe/models/detection_model.tflite"
      }
    }
  }

Trường node_options chấp nhận cú pháp proto3. Ngoài ra, bạn có thể chỉ định các tuỳ chọn tính toán trong trường options bằng cú pháp proto2.

  node {
    calculator: "TfLiteInferenceCalculator"
    input_stream: "TENSORS:main_model_input"
    output_stream: "TENSORS:main_model_output"
    node_options: {
      [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] {
        model_path: "mediapipe/models/detection_model.tflite"
      }
    }
  }

Không phải máy tính nào cũng chấp nhận các tuỳ chọn tính toán. Để chấp nhận các tuỳ chọn, máy tính thường sẽ xác định loại thông báo protobuf mới để biểu thị các tuỳ chọn, chẳng hạn như PacketClonerCalculatorOptions. Sau đó, máy tính sẽ đọc thông báo protobuf đó trong phương thức CalculatorBase::Open và có thể cũng trong hàm CalculatorBase::GetContract hoặc phương thức CalculatorBase::Process. Thông thường, loại thông báo protobuf mới sẽ được xác định là giản đồ protobuf bằng cách sử dụng tệp ".proto" và quy tắc bản dựng mediapipe_proto_library().

  mediapipe_proto_library(
      name = "packet_cloner_calculator_proto",
      srcs = ["packet_cloner_calculator.proto"],
      visibility = ["//visibility:public"],
      deps = [
          "//mediapipe/framework:calculator_options_proto",
          "//mediapipe/framework:calculator_proto",
      ],
  )

Công cụ tính mẫu

Phần này thảo luận về cách triển khai PacketClonerCalculator, thực hiện một công việc tương đối đơn giản và được sử dụng trong nhiều biểu đồ tính toán. PacketClonerCalculator chỉ tạo bản sao của các gói đầu vào gần đây nhất theo yêu cầu.

PacketClonerCalculator rất hữu ích khi dấu thời gian của các gói dữ liệu đến không được căn chỉnh hoàn hảo. Giả sử chúng ta có một căn phòng có micrô, cảm biến ánh sáng và máy quay video để thu thập dữ liệu cảm quan. Mỗi cảm biến hoạt động độc lập và thu thập dữ liệu không liên tục. Giả sử kết quả đầu ra của mỗi cảm biến là:

  • micrô = độ lớn tính theo đexiben âm thanh trong phòng (Số nguyên)
  • cảm biến ánh sáng = độ sáng của phòng (Số nguyên)
  • máy quay video = khung hình ảnh RGB của phòng (ImageFrame)

Quy trình nhận biết đơn giản của chúng tôi được thiết kế để xử lý dữ liệu cảm quan từ 3 cảm biến này sao cho bất cứ lúc nào khi chúng tôi có dữ liệu khung hình ảnh từ máy ảnh được đồng bộ hoá với dữ liệu thu thập gần đây nhất về âm lượng của micrô và dữ liệu về độ sáng của cảm biến ánh sáng. Để thực hiện việc này bằng MediaPipe, quy trình nhận biết của chúng tôi có 3 luồng đầu vào:

  • room_mic_signal – Mỗi gói dữ liệu trong luồng đầu vào này là dữ liệu số nguyên thể hiện mức âm thanh lớn trong một phòng có dấu thời gian.
  • room_lightening_sensor – Mỗi gói dữ liệu trong luồng đầu vào này là dữ liệu số nguyên thể hiện độ sáng của phòng bằng dấu thời gian.
  • room_video_tick_signal – Mỗi gói dữ liệu trong luồng đầu vào này là khung hình ảnh của dữ liệu video biểu thị video được thu thập từ camera trong phòng kèm theo dấu thời gian.

Dưới đây là cách triển khai PacketClonerCalculator. Bạn có thể thấy các phương thức GetContract(), Open()Process(), cũng như biến thực thể current_ chứa các gói đầu vào gần đây nhất.

// This takes packets from N+1 streams, A_1, A_2, ..., A_N, B.
// For every packet that appears in B, outputs the most recent packet from each
// of the A_i on a separate stream.

#include <vector>

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

namespace mediapipe {

// For every packet received on the last stream, output the latest packet
// obtained on all other streams. Therefore, if the last stream outputs at a
// higher rate than the others, this effectively clones the packets from the
// other streams to match the last.
//
// Example config:
// node {
//   calculator: "PacketClonerCalculator"
//   input_stream: "first_base_signal"
//   input_stream: "second_base_signal"
//   input_stream: "tick_signal"
//   output_stream: "cloned_first_base_signal"
//   output_stream: "cloned_second_base_signal"
// }
//
class PacketClonerCalculator : public CalculatorBase {
 public:
  static absl::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 absl::OkStatus();
  }

  absl::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 absl::OkStatus();
  }

  absl::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 absl::OkStatus();
  }

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

REGISTER_CALCULATOR(PacketClonerCalculator);
}  // namespace mediapipe

Thông thường, máy tính chỉ có tệp .cc. Không cần phải sử dụng miền .h vì mediapipe sử dụng phương thức đăng ký để giúp máy tính biết được. Sau khi bạn xác định lớp máy tính của mình, hãy đăng ký lớp đó bằng lệnh gọi macro Gias_CALCULATOR(computer_class_name).

Dưới đây là một biểu đồ MediaPipe nhỏ có 3 luồng đầu vào, 1 nút (PacketClonerCalculator) và 2 luồng đầu ra.

input_stream: "room_mic_signal"
input_stream: "room_lighting_sensor"
input_stream: "room_video_tick_signal"

node {
   calculator: "PacketClonerCalculator"
   input_stream: "room_mic_signal"
   input_stream: "room_lighting_sensor"
   input_stream: "room_video_tick_signal"
   output_stream: "cloned_room_mic_signal"
   output_stream: "cloned_lighting_sensor"
 }

Sơ đồ dưới đây cho thấy cách PacketClonerCalculator xác định các gói đầu ra (ở dưới cùng) dựa trên chuỗi gói đầu vào (trên cùng).

Vẽ đồ thị bằng PacketClonerCalculator
Mỗi lần nhận được một gói trên luồng đầu vào TICK, PacketClonerCalculator sẽ cho ra gói gần đây nhất từ từng luồng đầu vào. Trình tự của các gói đầu ra (ở dưới cùng) được xác định theo trình tự của các gói đầu vào (trên cùng) và dấu thời gian của các gói đó. Các dấu thời gian nằm ở bên phải biểu đồ.