גרפים

תרשים

פרוטו 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"
    }
    

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

אפשר לציין פרוטובוף של "אפשרויות גרף" לתרשים MediaPipe, שדומה ל-Calculator Options protobuf שצוין למחשבון 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 מקבל את אפשרות התרשים protobuf FaceDetectionOptions. השדה FaceDetectionOptions משמש להגדרה של ערכי שדות מסוימים באפשרויות המחשבון ImageToTensorCalculatorOptions, וחלק מערכי השדות באפשרויות InferenceCalculatorOptions בתרשים המשנה. ערכי השדות מוגדרים באמצעות התחביר option_value:.

ב-protobuf CalculatorGraphConfig::Node, השדות node_options: ו-option_value: מגדירים ביחד את ערכי האפשרויות למחשבון כמו ImageToTensorCalculator. השדה node_options: מגדיר קבוצה של ערכים קבועים ליטרליים באמצעות התחביר של text protobuf. כל שדה option_value: מגדיר את הערך של שדה Protobuf אחד באמצעות מידע מהתרשים המצורף, במיוחד מערכי השדות של אפשרויות התרשים של התרשים המצורף. בדוגמה שלמעלה, השדה 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 שמזהים הודעות ושדות מקננים של protobuf המופרדים באמצעות '/'. זה נקרא תחביר "ProtoPath". כדי שאפשר יהיה לעבור דרך option_value: הודעות מקננות שיש הפניה אליהן ב-LHS או ב-RHS, הן חייבות להיות מוגדרות כבר ב-protobuf המצורף.

מחזורים

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

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

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

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

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

הערה מהשוליים האחוריים

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

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

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

מנה ראשונית

כדי שאפשר יהיה להריץ את המחשבון המחשבון כאשר המספר השלם הראשון מגיע מהמקור של המספר השלם, נדרשת חבילה ראשונית עם הערך 0 ועם אותה חותמת זמן, בזרם הקלט של old_sum. הפלט של החבילה הראשונית הזו צריך להתבצע באמצעות מחשבון ההשהיה ב-method 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'
}