Skip to main content

Plug a Model Into Your Pipeline

Plug a Model Into Your Pipeline — animated walkthrough overview

FieldValue
DifficultyIntermediate
Estimated Read Time15 minutes
Labelsgraph, composition, patterns

Chapter 003 built a graph node-by-node. That is the most explicit path, but once you have a Model, you rarely want to hand-wire its internals. model.graph(...) hands you the model's pipeline as a group you drop into a Graph with one add(...). The interesting question is how much of the boundary that group brings with it — and that is what ModelRouteOptions controls.

This chapter contrasts two route configurations against the same model: a self-contained runnable graph that includes its own public input/output boundaries, and an attached graph that omits the input so it can hang off an upstream source (a camera, say) under an explicit name. By the end you will have composed both and printed each one's backend GStreamer string, so you can see exactly how the wiring differs.

Walkthrough

Compose a runnable model graph

The first pattern asks the model for a fully runnable graph. Setting include_input = true and include_output = true on the route options tells model.graph(opts) to inject explicit public input and output boundaries around the model group, so the resulting Graph can be built and run on its own with nothing else attached. graph.add(model.graph(opts)) is the whole composition — that single add is what every pattern reduces to underneath. Printing describe_backend() shows the generated GStreamer pipeline string.

Route options are Model::RouteOptions; the graph is simaai::neat::Graph.

tutorials/008_plug_model_into_pipeline/plug_model_into_pipeline.cpp
simaai::neat::Model::RouteOptions runnable_opt;
runnable_opt.include_input = true;
runnable_opt.include_output = true;
// CORE LOGIC
simaai::neat::Graph from_model;
from_model.add(model.graph(runnable_opt));
// END CORE LOGIC

Configure attach-time route options

The second pattern attaches the model under an upstream source rather than giving it its own input. Here include_input = false drops the public input boundary (the frames will come from elsewhere), include_output = true keeps the output, and upstream_name, name_suffix, and buffer_name make the wiring and element names explicit. Consistent naming like this is what keeps backend graphs readable and diagnosable in multi-camera or multi-model deployments.

tutorials/008_plug_model_into_pipeline/plug_model_into_pipeline.cpp
simaai::neat::Model::RouteOptions sopt;
sopt.include_input = false;
sopt.include_output = true;
sopt.upstream_name = "camera0";
sopt.name_suffix = "_camera0";
sopt.buffer_name = "camera0";

Attach the model group

With those options set, graph.add(model.graph(opts)) injects the same model group, now wired to attach to the named upstream instead of carrying its own source. It is the identical add call as the first pattern — only the route options changed — which is the point: composition is one operation, and ModelRouteOptions is the dial that decides what boundary the group brings with it.

Each variant prints its describe_backend() so you can compare the two backend strings; the file then also builds and runs a hand-wired direct Input -> Output graph to confirm the end-to-end path, printing direct_rank=.

tutorials/008_plug_model_into_pipeline/plug_model_into_pipeline.cpp
simaai::neat::Graph attached;
attached.add(model.graph(sopt));

Run

Run the Python and C++ (prebuilt) commands from the Neat install root (the directory that contains share/ and lib/); run the build from source commands from the repo root.

C++ (prebuilt):

./lib/sima-neat/tutorials/tutorial_008_plug_model_into_pipeline \
--model /tmp/yolo_v8s.tar.gz

C++ (build from source):

./build.sh --target tutorial_008_plug_model_into_pipeline
./build/tutorials-standalone/tutorial_008_plug_model_into_pipeline \
--model /tmp/yolo_v8s.tar.gz

Expected output (the C++ build prints each backend graph string, then the direct-graph rank):

model_graph_backend=
...
attached_graph_backend=
...
direct_rank=3
[OK] 008_plug_model_into_pipeline

(The Python build prints direct_graph_backend= followed by the backend string, then attached_graph_built=True.) To integrate this chapter's C++ source into your own project with a custom CMakeLists.txt (no extras folder required), see How to Run Tutorials on the landing page.

Full source

Show the complete C++ and Python programs
tutorials/008_plug_model_into_pipeline/plug_model_into_pipeline.cpp
// Three Graph composition patterns: direct nodes, model.graph(), attached Graph.
//
// Usage:
// tutorial_008_plug_model_into_pipeline [--model /path/to/model.tar.gz]

#include "neat.h"

#include <opencv2/core.hpp>

#include <filesystem>
#include <iostream>
#include <stdexcept>
#include <string>

namespace fs = std::filesystem;

namespace {

bool get_arg(int argc, char** argv, const std::string& key, std::string& out) {
for (int i = 1; i + 1 < argc; ++i) {
if (key == argv[i]) {
out = argv[i + 1];
return true;
}
}
return false;
}

} // namespace

int main(int argc, char** argv) {
try {
const int width = 224;
const int height = 224;

cv::Mat rgb(height, width, CV_8UC3, cv::Scalar(80, 40, 160));
if (!rgb.isContinuous())
rgb = rgb.clone();

// Pattern 1: build a Graph by adding Input/Output nodes directly.
simaai::neat::InputOptions in;
in.format = "RGB";
in.width = width;
in.height = height;
in.depth = 3;
in.do_timestamp = true;

// CORE LOGIC
simaai::neat::Graph direct;
direct.add(simaai::neat::nodes::Input(in));
direct.add(simaai::neat::nodes::Output());

std::string model_path;
if (get_arg(argc, argv, "--model", model_path) && fs::exists(model_path)) {
simaai::neat::Model model(model_path);

// Pattern 2: ask model.graph() to include explicit public Input/Output boundaries.
simaai::neat::Model::RouteOptions runnable_opt;
runnable_opt.include_input = true;
runnable_opt.include_output = true;
// CORE LOGIC
simaai::neat::Graph from_model;
from_model.add(model.graph(runnable_opt));
std::cout << "model_graph_backend=\n" << from_model.describe_backend() << "\n";

// Pattern 3: attach the model under an upstream name with custom options.
simaai::neat::Model::RouteOptions sopt;
sopt.include_input = false;
sopt.include_output = true;
sopt.upstream_name = "camera0";
sopt.name_suffix = "_camera0";
sopt.buffer_name = "camera0";

// CORE LOGIC
simaai::neat::Graph attached;
attached.add(model.graph(sopt));
std::cout << "attached_graph_backend=\n" << attached.describe_backend() << "\n";
}

simaai::neat::TensorList out = direct.run(std::vector<cv::Mat>{rgb});

if (out.empty())
throw std::runtime_error("direct Graph output missing tensor");
std::cout << "direct_rank=" << out.front().shape.size() << "\n";
std::cout << "[OK] 008_plug_model_into_pipeline\n";
return 0;
} catch (const std::exception& e) {
std::cerr << "[FAIL] " << e.what() << "\n";
return 1;
}
}

Source