גרפים

תרשים

אב הטיפוס CalculatorGraphConfig מציין את הטופולוגיה והפונקציונליות של תרשים MediaPipe. כל node בתרשים מייצג מחשבון מסוים ולציין את התצורות הנדרשות, כגון סוג המחשבון/המשנה, נתוני קלט, פלט ושדות אופציונליים, כמו אפשרויות ספציפיות לצמתים, מדיניות קלט והפעלה, המפורטות בהסבר סנכרון.

בשדה CalculatorGraphConfig יש כמה שדות אחרים להגדרה ברמת התרשים הגלובלית הגדרות, למשל הגדרות מפעיל הגרף, מספר השרשורים וגודל תור מקסימלי של זרמי קלט. כמה הגדרות ברמת התרשים שימושיות לכוונון של ביצועי התרשים בפלטפורמות שונות (למשל, מחשב לעומת נייד). עבור בנייד, צירוף של מחשבון מובנה להסקת מודלים יכול לשפר את הביצועים של אפליקציה בזמן אמת כי מאפשר את הרשות המוניציפאלית של שרשור.

בהמשך מוצגת דוגמה טריוויאלית CalculatorGraphConfig שבה יש לנו סדרה של מחשבוני תזוזה :

# This graph named main_pass_throughcals_nosubgraph.pbtxt contains 4
# passthrough calculators.
input_stream: "in"
output_stream: "out"
node {
    calculator: "PassThroughCalculator"
    input_stream: "in"
    output_stream: "out1"
}
node {
    calculator: "PassThroughCalculator"
    input_stream: "out1"
    output_stream: "out2"
}
node {
    calculator: "PassThroughCalculator"
    input_stream: "out2"
    output_stream: "out3"
}
node {
    calculator: "PassThroughCalculator"
    input_stream: "out3"
    output_stream: "out"
}

MediaPipe מציע ייצוג C++ חלופי לתרשימים מורכבים (למשל צינורות עיבוד נתונים של למידת מכונה, מטא-נתונים של מודל טיפול, צמתים אופציונליים וכו'). הגרף שלמעלה עשוי להיראות כך:

CalculatorGraphConfig BuildGraphConfig() {
  Graph graph;

  // Graph inputs
  Stream<AnyType> in = graph.In(0).SetName("in");

  auto pass_through_fn = [](Stream<AnyType> in,
                            Graph& graph) -> Stream<AnyType> {
    auto& node = graph.AddNode("PassThroughCalculator");
    in.ConnectTo(node.In(0));
    return node.Out(0);
  };

  Stream<AnyType> out1 = pass_through_fn(in, graph);
  Stream<AnyType> out2 = pass_through_fn(out1, graph);
  Stream<AnyType> out3 = pass_through_fn(out2, graph);
  Stream<AnyType> out4 = pass_through_fn(out3, graph);

  // Graph outputs
  out4.SetName("out").ConnectTo(graph.Out(0));

  return graph.GetConfig();
}

ניתן לקבל מידע נוסף במאמר יצירת תרשימים ב-C++.

תרשים משנה

להפוך CalculatorGraphConfig למודולים משנה ולסייע בשימוש חוזר של פתרונות התפיסה, ניתן להגדיר תרשים של MediaPipe כ-Subgraph. ממשק ציבורי של תת-תרשים מורכב מקבוצה של שידורי קלט ופלט דומה לממשק הציבורי של מחשבון. לאחר מכן ניתן לכלול את תת-גרף CalculatorGraphConfig, כאילו היה מחשבון. כאשר תרשים MediaPipe שנטען מ-CalculatorGraphConfig, כל צומת של תרשים משנה מוחלף ב- של המחשבונים המתאימים. כתוצאה מכך, הסמנטיקה והביצועים של התרשים זהה לתרשים התואם של המחשבונים.

בהמשך מוצגת דוגמה לאופן שבו ניתן ליצור תת-תרשים בשם TwoPassThroughSubgraph.

  1. הגדרת המשנה

    # This subgraph is defined in two_pass_through_subgraph.pbtxt
    # and is registered as "TwoPassThroughSubgraph"
    
    type: "TwoPassThroughSubgraph"
    input_stream: "out1"
    output_stream: "out3"
    
    node {
        calculator: "PassThroughCalculator"
        input_stream: "out1"
        output_stream: "out2"
    }
    node {
        calculator: "PassThroughCalculator"
        input_stream: "out2"
        output_stream: "out3"
    }
    

    הממשק הציבורי לתוכן המשנה מורכב מהרכיבים הבאים:

    • הציגו זרמי קלט בגרף
    • הציגו בגרף זרמי פלט
    • הצגת חבילות צדדיות לקלט בגרף
    • הציגו בגרף חבילות צדדיות של פלט
  2. רושמים את המשנה באמצעות כלל BUILD mediapipe_simple_subgraph. הפרמטר register_as מגדיר את שם הרכיב של תרשים המשנה החדש.

    # Small section of BUILD file for registering the "TwoPassThroughSubgraph"
    # subgraph for use by main graph main_pass_throughcals.pbtxt
    
    mediapipe_simple_subgraph(
        name = "twopassthrough_subgraph",
        graph = "twopassthrough_subgraph.pbtxt",
        register_as = "TwoPassThroughSubgraph",
        deps = [
                "//mediapipe/calculators/core:pass_through_calculator",
                "//mediapipe/framework:calculator_graph",
        ],
    )
    
  3. השתמשו בתרשים המשנה בתרשים הראשי.

    # This main graph is defined in main_pass_throughcals.pbtxt
    # using subgraph called "TwoPassThroughSubgraph"
    
    input_stream: "in"
    node {
        calculator: "PassThroughCalculator"
        input_stream: "in"
        output_stream: "out1"
    }
    node {
        calculator: "TwoPassThroughSubgraph"
        input_stream: "out1"
        output_stream: "out3"
    }
    node {
        calculator: "PassThroughCalculator"
        input_stream: "out3"
        output_stream: "out4"
    }
    

אפשרויות תרשים

אפשר לציין 'אפשרויות תרשים' Protobuf לתרשים MediaPipe דומה ל-Calculator Options aprotobuf מוגדר למחשבון MediaPipe. "אפשרויות הגרף" האלה יכול להיות ולציין איפה מופעל גרף, ומשמש לאכלוס אפשרויות המחשבון בתרשים עצמו.

ב-CalculatorGraphConfig, ניתן לציין אפשרויות תרשים עבור תרשים משנה בדיוק כמו אפשרויות המחשבון, כפי שמוצג בהמשך:

node {
  calculator: "FlowLimiterCalculator"
  input_stream: "image"
  output_stream: "throttled_image"
  node_options: {
    [type.googleapis.com/mediapipe.FlowLimiterCalculatorOptions] {
      max_in_flight: 1
    }
  }
}

node {
  calculator: "FaceDetectionSubgraph"
  input_stream: "IMAGE:throttled_image"
  node_options: {
    [type.googleapis.com/mediapipe.FaceDetectionOptions] {
      tensor_width: 192
      tensor_height: 192
    }
  }
}

ב-CalculatorGraphConfig, אפשר לקבל אפשרויות תרשים ולהשתמש בהן לאכלוס המחשבון, כפי שמוצג בהמשך:

graph_options: {
  [type.googleapis.com/mediapipe.FaceDetectionOptions] {}
}

node: {
  calculator: "ImageToTensorCalculator"
  input_stream: "IMAGE:image"
  node_options: {
    [type.googleapis.com/mediapipe.ImageToTensorCalculatorOptions] {
        keep_aspect_ratio: true
        border_mode: BORDER_ZERO
    }
  }
  option_value: "output_tensor_width:options/tensor_width"
  option_value: "output_tensor_height:options/tensor_height"
}

node {
  calculator: "InferenceCalculator"
  node_options: {
    [type.googleapis.com/mediapipe.InferenceCalculatorOptions] {}
  }
  option_value: "delegate:options/delegate"
  option_value: "model_path:options/model_path"
}

בדוגמה הזו, ה-FaceDetectionSubgraph מקבל פרוטובוט של אפשרות תרשים FaceDetectionOptions. השדה FaceDetectionOptions משמש להגדרת שדות מסוימים ערכים באפשרויות המחשבון ImageToTensorCalculatorOptions ושדות מסוימים ערכים באפשרויות המשנה InferenceCalculatorOptions. ערכי השדות מוגדרות באמצעות התחביר option_value:.

ב-CalculatorGraphConfig::Node Protobuf, השדות node_options: ו- option_value: מגדירים יחד את ערכי האפשרויות של מחשבון, כמו ImageToTensorCalculator השדה node_options: מגדיר קבוצה של מילים קבועים באמצעות התחביר של text protobuf. כל שדה option_value: מגדיר את הערך של שדה אב-טיפוס אחד באמצעות מידע מהצורך באופן ספציפי מערכי השדות של אפשרויות התרשים של גרפי. בדוגמה שלמעלה, השדה option_value: השדה "output_tensor_width:options/tensor_width" מוגדר ImageToTensorCalculatorOptions.output_tensor_width באמצעות הערך של FaceDetectionOptions.tensor_width

התחביר של option_value: דומה לתחביר של input_stream:. התחביר הוא option_value: "LHS:RHS". ה-LHS מזהה אפשרות של מחשבון וה-RHS מזהה שדה של אפשרות בתרשים. וליתר דיוק, צוות ה-LHS ו-RHS כל אחד מהם מורכב מסדרה של שמות שדות אב-טיפוס שמזהים הודעות ושדות ב-protobuf, מופרדים על ידי '/'. הוא נקרא ProtoPath תחביר. הודעות מקוננות שיש אליהן הפניה ב-LHS או ב-RHS כבר צריכות להיות מוגדר ב-protobuf המקיף כך שניתן לחצות אותו באמצעות option_value:

מחזורים

כברירת מחדל, MediaPipe דורש שהתרשימים של המחשבון יהיו אציקליים ומטפלים במחזורים בתרשים כשגיאות. אם התרשים מיועד לכלול מחזורים, המחזורים צריכים יופיע בתצורה של התרשים. בדף הזה נסביר איך לעשות את זה.

הערה: הגישה הנוכחית היא ניסיונית וכפופה לשינויים. נשמח לראות משוב.

יש להשתמש בבדיקת היחידה של CalculatorGraphTest.Cycle mediapipe/framework/calculator_graph_test.cc כקוד לדוגמה. למטה מוצגת את הגרף המחזורי בבדיקה. הפלט של sum של כלי הכתיבה הוא הסכום של של מספרים שלמים שנוצרו על ידי מחשבון המקור של מספרים שלמים.

גרף מחזורי שמוסיף זרם של מספרים שלמים

התרשים הפשוט הזה ממחיש את כל הבעיות שתומכים בתרשימים מחזוריים.

הוספת הערות לקצה האחורי

אנו דורשים שכל קצה של המחזור יסומן כקצה אחורי. כך אפשר המיון הטופולוגי של MediaPipe פועל אחרי הסרת כל הקצוות האחוריים.

בדרך כלל יש כמה דרכים לבחור את הצדדים האחוריים. אילו קצוות מסומנים כי הקצוות האחוריים משפיעים על הצמתים שנחשבים ל-upstream ואילו הצמתים נחשב כמורד הזרם, וזה משפיע על העדיפויות ש-MediaPipe מקצה לצמתים.

לדוגמה, הבדיקה CalculatorGraphTest.Cycle מסמנת את הקצה old_sum הקצה האחורי, כך שהצומת Delay נחשב כצומת במורד הזרם (downstream) של מבצע ההוספה מקבל עדיפות גבוהה יותר. לחלופין, אפשר לסמן את sum קלט לצומת ההשהיה בתור הקצה האחורי, ובמקרה כזה הצומת נחשב כצומת ב-upstream של הצומת של ההוספה, ומקבלים עדיפות נמוכה יותר.

מנה ראשונית

כדי שאפשר יהיה להריץ את המחשבון השלם הראשון מתוך המספר השלם מגיע, אנחנו צריכים חבילה ראשונית, עם ערך 0 ועם אותו timestamp, ב-old_sum של הקלט למוסיף. החבילה הראשונית הזו הפלט אמור להתקבל על ידי מחשבון העיכוב בשיטה Open().

עיכוב בלולאה

בכל לולאה צריכה להיות השהיה כדי ליישר את הפלט הקודם של sum עם של מספר שלם. הפעולה הזו מתבצעת גם על ידי הצומת ההשהיה. אז הצומת ההשהיה צריך את הדברים הבאים לגבי חותמות הזמן של מחשבון המקור השלם:

  • חותמת הזמן של הפלט הראשון

  • דלתא של חותמת הזמן בין פלטים עוקבים.

אנחנו מתכננים להוסיף מדיניות חלופית לתזמון שמתייחסת רק לחבילה ההזמנה ומתעלמת מחותמות זמן של חבילות, דבר שיבטל את אי הנוחות.

סיום מוקדם של המחשבון כשמוגדר מקור קלט אחד

כברירת מחדל, MediaPipe קורא לשיטה Close() של מחשבון שאינו מקור כאשר כל זרמי הקלט שלו הסתיימו. בגרף לדוגמה, אנחנו רוצים להפסיק הוספת צומת ברגע שמקור המספר השלם מסתיים. להשיג את זה הגדרת צומת ההוספה באמצעות handler של זרם קלט חלופי EarlyCloseInputStreamHandler

קוד מקור רלוונטי

מחשבון השהיה

שימו לב לקוד ב-Open() שמפיק את החבילה הראשונית ולקוד Process() שמוסיף עיכוב של (יחידה) לחבילות הקלט. כפי שצוין למעלה, עיכוב מסוים מניח שזרם הפלט שלו משמש לצד זרם קלט עם חותמות זמן של מנות 0, 1, 2, 3, ...

class UnitDelayCalculator : public Calculator {
 public:
  static absl::Status FillExpectations(
      const CalculatorOptions& extendable_options, PacketTypeSet* inputs,
      PacketTypeSet* outputs, PacketTypeSet* input_side_packets) {
    inputs->Index(0)->Set<int>("An integer.");
    outputs->Index(0)->Set<int>("The input delayed by one time unit.");
    return absl::OkStatus();
  }

  absl::Status Open() final {
    Output()->Add(new int(0), Timestamp(0));
    return absl::OkStatus();
  }

  absl::Status Process() final {
    const Packet& packet = Input()->Value();
    Output()->AddPacket(packet.At(packet.Timestamp().NextAllowedInStream()));
    return absl::OkStatus();
  }
};

הגדרת התרשים

חשוב לשים לב להערה back_edge ולהערה החלופית input_stream_handler.

node {
  calculator: 'GlobalCountSourceCalculator'
  input_side_packet: 'global_counter'
  output_stream: 'integers'
}
node {
  calculator: 'IntAdderCalculator'
  input_stream: 'integers'
  input_stream: 'old_sum'
  input_stream_info: {
    tag_index: ':1'  # 'old_sum'
    back_edge: true
  }
  output_stream: 'sum'
  input_stream_handler {
    input_stream_handler: 'EarlyCloseInputStreamHandler'
  }
}
node {
  calculator: 'UnitDelayCalculator'
  input_stream: 'sum'
  output_stream: 'old_sum'
}