Skip to content

Commit b1de303

Browse files
authored
doc: add the Hello World Tutorial (#1330)
1 parent 8820a8f commit b1de303

File tree

2 files changed

+332
-3
lines changed

2 files changed

+332
-3
lines changed

Assets/MediaPipeUnity/Tutorial/Hello World/HelloWorld.cs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,49 @@
44
// license that can be found in the LICENSE file or at
55
// https://opensource.org/licenses/MIT.
66

7-
// ATTENTION!: This code is for a tutorial and it's broken as is.
8-
97
using UnityEngine;
108

119
namespace Mediapipe.Unity.Tutorial
1210
{
13-
public class HelloWorld : MonoBehaviour { }
11+
public class HelloWorld : MonoBehaviour
12+
{
13+
private void Start()
14+
{
15+
var configText = @"
16+
input_stream: ""in""
17+
output_stream: ""out""
18+
node {
19+
calculator: ""PassThroughCalculator""
20+
input_stream: ""in""
21+
output_stream: ""out1""
22+
}
23+
node {
24+
calculator: ""PassThroughCalculator""
25+
input_stream: ""out1""
26+
output_stream: ""out""
27+
}
28+
";
29+
var graph = new CalculatorGraph(configText);
30+
using var poller = graph.AddOutputStreamPoller<string>("out");
31+
graph.StartRun();
32+
33+
for (var i = 0; i < 10; i++)
34+
{
35+
var input = Packet.CreateStringAt("Hello World!", i);
36+
graph.AddPacketToInputStream("in", input);
37+
}
38+
39+
graph.CloseInputStream("in");
40+
41+
using var output = new Packet<string>();
42+
while (poller.Next(output))
43+
{
44+
Debug.Log(output.Get());
45+
}
46+
47+
graph.WaitUntilDone();
48+
49+
Debug.Log("Done");
50+
}
51+
}
1452
}

docs/Tutorial-Hello-World.md

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
# Hello World!
2+
3+
> :bell: If you're new to MediaPipe, consider reading the [Framework Concepts](https://ai.google.dev/edge/mediapipe/framework/framework_concepts/overview) article first.
4+
5+
> :skull_and_crossbones: On Windows, some of the code below might cause UnityEditor to crash. Check [Technical Limitations](../README.md#warning-technical-limitations) for more information.
6+
7+
Let's write our first program!
8+
9+
> :bell: The following code is based on [mediapipe/examples/desktop/examples/hello_world/hello_world.cc](https://github.com/google/mediapipe/blob/cf101e62a9d49a51be76836b2b8e5ba5c06b5da0/mediapipe/examples/desktop/hello_world/hello_world.cc).
10+
11+
> :bell: You can find the complete code at [Tutorial/Hello World](https://github.com/homuler/MediaPipeUnityPlugin/tree/master/Assets/MediaPipeUnity/Tutorial/Hello%20World).
12+
13+
## Send the input
14+
15+
To use the Calculators provided by MediaPipe, we typically need to set up a [`CalculatorGraph`](https://ai.google.dev/edge/mediapipe/framework/framework_concepts/graphs). Let's start with that!
16+
17+
> :bell: Each `CalculatorGraph` requires its own configuration ([`CalculatorGraphConfig`](https://ai.google.dev/edge/mediapipe/framework/framework_concepts/graphs#graph_config)).
18+
19+
```cs
20+
var configText = @"
21+
input_stream: ""in""
22+
output_stream: ""out""
23+
node {
24+
calculator: ""PassThroughCalculator""
25+
input_stream: ""in""
26+
output_stream: ""out1""
27+
}
28+
node {
29+
calculator: ""PassThroughCalculator""
30+
input_stream: ""out1""
31+
output_stream: ""out""
32+
}
33+
";
34+
35+
var graph = new CalculatorGraph(configText);
36+
```
37+
38+
To run a `CalculatorGraph`, call the `StartRun` method.
39+
40+
```cs
41+
graph.StartRun();
42+
```
43+
44+
Note that the `StartRun` method will throw an exception if there is an error.
45+
46+
Now that we've started the graph, let's provide some input to the `CalculatorGraph`.
47+
48+
Let's say we want to give a sequence of 10 strings (`"Hello World!"`) as input.
49+
50+
```cs
51+
for (var i = 0; i < 10; i++)
52+
{
53+
// Send input to running graph
54+
}
55+
```
56+
57+
In MediaPipe, inputs are passed through a class called [`Packet`](https://ai.google.dev/edge/mediapipe/framework/framework_concepts/packets).
58+
59+
```cs
60+
var input = Packet.CreateString("Hello World!");
61+
```
62+
63+
To pass an input `Packet` to the `CalculatorGraph`, we use the `AddPacketToInputStream` method.\
64+
In this example, our `CalculatorGraph` has a single input stream named `in`.
65+
66+
> :bell: It depends on the `CalculatorGraphConfig`. `CalculatorGraph` can have multiple input streams
67+
68+
```cs
69+
for (var i = 0; i < 10; i++)
70+
{
71+
var input = Packet.CreateString("Hello World!");
72+
graph.AddPacketToInputStream("in", input); // NOTE: Packet is disposed automatically
73+
}
74+
```
75+
76+
`CalculatorGraph#AddPacketToInputStream` may also throw an exception, for instance, when the input type is invalid.
77+
78+
When we're finished, we need to:
79+
80+
1. Close all input streams
81+
2. Dispose of the `CalculatorGraph`
82+
83+
Here's how we do that:
84+
85+
```cs
86+
using var graph = new CalculatorGraph(configText);
87+
// ...
88+
graph.CloseInputStream("in");
89+
graph.WaitUntilDone();
90+
```
91+
92+
For now, let's just run the code we've written so far.
93+
94+
Save the following code as `HelloWorld.cs`, attach it to an empty `GameObject` and play the scene.
95+
96+
```cs
97+
using UnityEngine;
98+
99+
namespace Mediapipe.Unity.Tutorial
100+
{
101+
public class HelloWorld : MonoBehaviour
102+
{
103+
private void Start()
104+
{
105+
var configText = @"
106+
input_stream: ""in""
107+
output_stream: ""out""
108+
node {
109+
calculator: ""PassThroughCalculator""
110+
input_stream: ""in""
111+
output_stream: ""out1""
112+
}
113+
node {
114+
calculator: ""PassThroughCalculator""
115+
input_stream: ""out1""
116+
output_stream: ""out""
117+
}
118+
";
119+
using var graph = new CalculatorGraph(configText);
120+
graph.StartRun();
121+
122+
for (var i = 0; i < 10; i++)
123+
{
124+
var input = Packet.CreateString("Hello World!");
125+
graph.AddPacketToInputStream("in", input);
126+
}
127+
128+
graph.CloseInputStream("in");
129+
graph.WaitUntilDone();
130+
131+
Debug.Log("Done");
132+
}
133+
}
134+
}
135+
```
136+
137+
![hello-world-timestamp-error](https://github.com/user-attachments/assets/4fc4416f-b254-4612-aa1b-b973a0dc7073)
138+
139+
Oops, an error occurred.
140+
141+
```txt
142+
BadStatusException: INVALID_ARGUMENT: Graph has errors:
143+
; In stream "in", timestamp not specified or set to illegal value: Timestamp::Unset()
144+
Mediapipe.Status.AssertOk () (at ./Packages/com.github.homuler.mediapipe/Runtime/Scripts/Framework/Port/Status.cs:168)
145+
Mediapipe.MpResourceHandle.AssertStatusOk (System.IntPtr statusPtr) (at ./Packages/com.github.homuler.mediapipe/Runtime/Scripts/Core/MpResourceHandle.cs:115)
146+
Mediapipe.CalculatorGraph.AddPacketToInputStream[T] (System.String streamName, Mediapipe.Packet`1[TValue] packet) (at ./Packages/com.github.homuler.mediapipe/Runtime/Scripts/Framework/CalculatorGraph.cs:167)
147+
Mediapipe.Unity.Tutorial.HelloWorld.Start () (at Assets/MediaPipeUnity/Tutorial/Hello World/HelloWorld.cs:35)
148+
```
149+
150+
Each input packet [should have a timestamp](https://ai.google.dev/edge/mediapipe/framework/framework_concepts/packets), but it does not appear to be set.
151+
152+
Let's fix the code that initializes a `Packet` as follows:
153+
154+
```cs
155+
// var input = Packet.CreateString("Hello World!");
156+
var input = Packet.CreateStringAt("Hello World!", i);
157+
```
158+
159+
![hello-world-no-output](https://github.com/user-attachments/assets/d87247ef-2b13-4f0b-8c5d-4a652ec05f01)
160+
161+
This time it seems to be working.\
162+
But wait, we are not receiving the `CalculatorGraph` output!
163+
164+
## Get the output
165+
166+
To receive output from the `CalculatorGraph`, we need to set up an output stream poller before running the graph.\
167+
In this example, our graph has a single output stream named `out`.
168+
169+
> :bell: It depends on the `CalculatorGraphConfig`. `CalculatorGraph` can have multiple output streams.
170+
171+
```cs
172+
using var graph = new CalculatorGraph(configText);
173+
174+
// Initialize an `OutputStreamPoller`. Note that the output type is string.
175+
using var poller = graph.AddOutputStreamPoller<string>("out");
176+
177+
graph.StartRun();
178+
```
179+
180+
Then, we can get output using the `OutputStreamPoller#Next`.\
181+
Like inputs, outputs must be received through packets.
182+
183+
```cs
184+
graph.CloseInputStream("in");
185+
186+
// Initialize an empty packet
187+
using var output = new Packet<string>();
188+
189+
while (poller.Next(output))
190+
{
191+
Debug.Log(output.Get());
192+
}
193+
194+
graph.WaitUntilDone();
195+
```
196+
197+
Now, our code would look as follows.
198+
Note that `OutputStreamPoller` and `Packet` also need to be disposed of.
199+
200+
```cs
201+
using UnityEngine;
202+
203+
namespace Mediapipe.Unity.Tutorial
204+
{
205+
public class HelloWorld : MonoBehaviour
206+
{
207+
private void Start()
208+
{
209+
var configText = @"
210+
input_stream: ""in""
211+
output_stream: ""out""
212+
node {
213+
calculator: ""PassThroughCalculator""
214+
input_stream: ""in""
215+
output_stream: ""out1""
216+
}
217+
node {
218+
calculator: ""PassThroughCalculator""
219+
input_stream: ""out1""
220+
output_stream: ""out""
221+
}
222+
";
223+
using var graph = new CalculatorGraph(configText);
224+
using var poller = graph.AddOutputStreamPoller<string>("out");
225+
graph.StartRun();
226+
227+
for (var i = 0; i < 10; i++)
228+
{
229+
var input = Packet.CreateStringAt("Hello World!", i);
230+
graph.AddPacketToInputStream("in", input);
231+
}
232+
233+
graph.CloseInputStream("in");
234+
235+
using var output = new Packet<string>();
236+
while (poller.Next(output))
237+
{
238+
Debug.Log(output.Get());
239+
}
240+
241+
graph.WaitUntilDone();
242+
243+
Debug.Log("Done");
244+
}
245+
}
246+
}
247+
```
248+
249+
![hello-world-output](https://github.com/user-attachments/assets/60bddb98-ef95-4806-b2f6-ec6ba22be0e4)
250+
251+
## Validate the config format
252+
253+
What happens if the config format is invalid?
254+
255+
```cs
256+
using var graph = new CalculatorGraph("invalid format");
257+
```
258+
259+
![hello-world-invalid-config](https://github.com/user-attachments/assets/75220066-0532-4aef-94dc-eb40a094bcbd)
260+
261+
Hmm, the constructor fails, which is probably the behavior it should be.\
262+
Let's check [`Editor.log`](https://docs.unity3d.com/Manual/LogFiles.html).
263+
264+
```txt
265+
[libprotobuf ERROR external/com_google_protobuf/src/google/protobuf/text_format.cc:335] Error parsing text-format mediapipe.CalculatorGraphConfig: 1:9: Message type "mediapipe.CalculatorGraphConfig" has no field named "invalid".
266+
MediaPipeException: Failed to parse config text. See error logs for more details
267+
at Mediapipe.CalculatorGraphConfigExtension.ParseFromTextFormat (Google.Protobuf.MessageParser`1[T] _, System.String configText) [0x0001e] in ./Packages/com.github.homuler.mediapipe/Runtime/Scripts/Framework/CalculatorGraphConfigExtension.cs:21
268+
at Mediapipe.CalculatorGraph..ctor (System.String textFormatConfig) [0x00000] in ./Packages/com.github.homuler.mediapipe/Runtime/Scripts/Framework/CalculatorGraph.cs:33
269+
at Mediapipe.Unity.Tutorial.HelloWorld.Start () [0x00000] in /home/homuler/Development/unity/MediaPipeUnityPlugin/Assets/MediaPipeUnity/Tutorial/Hello World/HelloWorld.cs:29
270+
```
271+
272+
Not too bad, but it's inconvenient to check `Editor.log` every time.\
273+
Let's fix it so that the logs are visible in the Console Window.
274+
275+
```cs
276+
Protobuf.SetLogHandler(Protobuf.DefaultLogHandler);
277+
var graph = new CalculatorGraph("invalid format");
278+
```
279+
280+
![hello-world-protobuf-logger](https://github.com/user-attachments/assets/03956a07-ba77-45a0-9479-bb5f3dd8b2c1)
281+
282+
Great!\
283+
However, there's an important detail to handle: we need to reset the log handler when the application exits to prevent potential crashes (SIGSEGV).\
284+
Add this code to restore the default `LogHandler`:
285+
286+
```cs
287+
void OnApplicationQuit()
288+
{
289+
Protobuf.ResetLogHandler();
290+
}
291+
```

0 commit comments

Comments
 (0)