Advanced: your own nodes in the editor¶
The Custom Model node of the editor draws the usual results of a model: bounding boxes with label and confidence, keypoints, and the top classes of a classification. When your post-processing (custom pre- and post-processing) returns something else (an image, a map, values to show in your own way), add a node of your own to the editor that turns it into what you want to see.
How it works¶
What your postprocess() returns arrives in the data field of the server's response. The model node passes the
whole response on its first output, predictions / data:
{"status": "success", "topic": "my_model", "data": { ...what postprocess() returned... }, "execution_time": 10.3}
Your node reads it from its input, builds what you want to see, and passes it to its output: an image, for example, which you connect to MuseBox Output → Image Rendering.
A node is a JavaScript file that registers a node type with LiteGraph,
the graph library of the editor: its inputs and outputs, and onExecute(), called at every step of the flow.
Add a node¶
The editor folder is ~/musebox/no-code-flow-editor (C:\Users\<your-user>\musebox\no-code-flow-editor on Windows),
the folder where you run serve.py.
- In the editor folder, create
nodes/user/and your file in it, for examplenodes/user/result-image-view.js(the example below). -
Open
index.htmlin the editor folder and add a line for your file after the line ofcustom-model.js:<script type="text/javascript" src="/nodes/musebox-tasks/custom-model.js"></script> <script type="text/javascript" src="/nodes/user/result-image-view.js"></script> -
Reload the editor page. The node is in right-click → Add Node → My Nodes.
- Connect it: model node predictions / data → your node → Image Rendering.
The browser console (F12) shows the errors of your file, if any.
Example: show an image returned by the post-processing¶
The post-processing draws the image itself in C++ with OpenCV (a colour map, overlays, text) and returns it as
a JPEG in base64, in the field image of its result:
// in postprocess(): `view` is the cv::Mat (BGR) you want to see
std::vector<unsigned char> jpeg;
cv::imencode(".jpg", view, jpeg, {cv::IMWRITE_JPEG_QUALITY, 90});
return {
{"image", to_base64(jpeg)}, // standard base64, without the "data:" prefix
{"width", view.cols},
{"height", view.rows}
};
to_base64 is not part of the SDK: copy this function into postprocess.cpp, above postprocess():
static std::string to_base64(const std::vector<unsigned char>& in) {
static const char* t = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string out;
out.reserve((in.size() + 2) / 3 * 4);
size_t i = 0;
for (; i + 2 < in.size(); i += 3) {
const unsigned v = (in[i] << 16) | (in[i + 1] << 8) | in[i + 2];
out += t[v >> 18]; out += t[(v >> 12) & 63]; out += t[(v >> 6) & 63]; out += t[v & 63];
}
if (i < in.size()) {
unsigned v = in[i] << 16;
if (i + 1 < in.size()) v |= in[i + 1] << 8;
out += t[v >> 18]; out += t[(v >> 12) & 63];
out += (i + 1 < in.size()) ? t[(v >> 6) & 63] : '=';
out += '=';
}
return out;
}
A JPEG is also much lighter than the raw values: a 224 × 224 image is about 15 KB, the same 50 176 values as JSON numbers about 0.5 MB per frame.
The editor node decodes the image and passes it on (nodes/user/result-image-view.js):
// Result Image View: shows an image (base64 JPEG / PNG) that a custom post-processing returns in its result.
// Input: "predictions / data" of the model node. Output: the image, for Image Rendering.
(function (global) {
var LiteGraph = global.LiteGraph;
function ResultImageView() {
this.addInput("model data", "array");
this.addOutput("image", "image");
this.properties = { field: "image" }; // the key of the image inside "data"
this.addWidget("text", "Field", this.properties.field, "field");
this.image = null; // the last decoded image
this.lastB64 = null;
}
ResultImageView.title = "Result Image View";
ResultImageView.desc = "Shows an image (base64 JPEG / PNG) returned by a custom post-processing";
ResultImageView.prototype.onExecute = function () {
var response = this.getInputData(0);
var b64 = response && response.data && response.data[this.properties.field];
if (typeof b64 === "string" && b64 !== this.lastB64) { // a new result: decode it
this.lastB64 = b64;
var img = new Image();
var node = this;
img.onload = function () { node.image = img; };
img.src = (b64.indexOf("data:") === 0 ? "" : "data:image/jpeg;base64,") + b64;
}
if (this.image) this.setOutputData(0, this.image);
};
LiteGraph.registerNodeType("My Nodes/Result Image View", ResultImageView);
})(this);
The Field widget of the node chooses the key of the image in data, if your post-processing uses another
name. To draw on the camera frame instead, give the node a second input for the frame and draw both on a
<canvas> (the Scale node in nodes/musebox-operators.js shows how to create one).
Your application: the client does not know your node
The MuseBox Client (chapter 5) runs only the MuseBox Tasks nodes of a saved pipeline:
your nodes are ignored, and nothing of what they do in the editor reaches your application by itself. For a
model it does not know, the client draws only an image found in data.image, blended over the frame; any
other display must be written again in your application:
- Python client:
musebox-client/python/pipeline_runner.py,_run_root_task(), which chooses how to draw each result by its topic; - C++ client:
musebox-client/cpp/src/pipeline_runner.cpp, the same choice inPipelineRunner::run(); - your own code: read
datafrom the response (the messages) and draw it as you need.
Editor updates replace index.html
The editor's Update button (chapter 4) replaces the editor's own files,
index.html included: after an update, add the line of step 2 again. Your files in nodes/user/ stay, and the
previous index.html is kept in .musebox-previous/. A saved pipeline that uses your node opens correctly
only when your file is loaded.