Creating a New Custom Module in ApraPipes: A Complete Developer Guide
To create a custom module in ApraPipes, you must define a C++ class derived from Module with a corresponding *Props class, register it in ModuleRegistrations.cpp using the declarative registry, and rebuild the library to make it available via JSON pipelines or the Node.js API.
ApraPipes is a high-performance computer vision and image processing framework developed by Apra Labs. Creating a new custom module in ApraPipes allows you to extend the framework's capabilities with proprietary image processing algorithms while maintaining compatibility with the declarative JSON pipeline syntax and Node.js bindings.
Step 1 – Define the Module Implementation and Props Class
Every module requires two components: a properties class (*Props) that inherits from ModuleProps, and the module class itself that inherits from Module. The properties class handles serialization and dynamic property updates, while the module class implements the processing logic.
Creating the Header File
Place your header in base/include/<YourModule>.h. The following example demonstrates a MyBlur module with a configurable radius property:
// base/include/MyBlur.h
#pragma once
#include "Module.h"
#include "declarative/PropertyMacros.h"
class MyBlurProps : public ModuleProps {
public:
int radius = 3; // default value
// --------------------------------------------------------------------
// Property binding for declarative pipelines
// --------------------------------------------------------------------
template<typename PropsT>
static void applyProperties(
PropsT& props,
const std::map<std::string, apra::ScalarPropertyValue>& values,
std::vector<std::string>& missingRequired)
{
apra::applyProp(props.radius, "radius", values, true, missingRequired);
}
// Optional dynamic‑property support (runtime change)
static std::vector<std::string> dynamicPropertyNames() { return {"radius"}; }
apra::ScalarPropertyValue getProperty(const std::string& name) const {
if (name == "radius") return static_cast<double>(radius);
throw std::runtime_error("Unknown property: " + name);
}
bool setProperty(const std::string& name, const apra::ScalarPropertyValue& value) {
if (name == "radius") { radius = static_cast<int>(std::get<double>(value)); return true; }
return false;
}
};
class MyBlur : public Module {
public:
MyBlur(MyBlurProps p);
~MyBlur() override = default;
bool init() override;
bool term() override;
void setProps(MyBlurProps& p);
MyBlurProps getProps();
protected:
bool process(frame_container& frames) override;
private:
class Detail;
boost::shared_ptr<Detail> mDetail;
};
Implementing the Source File
Place your implementation in base/src/<YourModule>.cpp. This file contains the constructor, lifecycle methods, and the process() function that executes the actual frame transformation:
// base/src/MyBlur.cpp
#include "MyBlur.h"
class MyBlur::Detail {
public:
MyBlurProps mProps;
Detail(const MyBlurProps& p) : mProps(p) {}
void setProps(const MyBlurProps& p) { mProps = p; }
};
MyBlur::MyBlur(MyBlurProps p) : Module(TRANSFORM, "MyBlur", p), mDetail(new Detail(p)) {}
bool MyBlur::init() { return true; }
bool MyBlur::term() { return true; }
void MyBlur::setProps(MyBlurProps& p) {
if (!canQueueProps()) { mDetail->setProps(p); }
else { Module::addPropsToQueue(p); }
}
MyBlurProps MyBlur::getProps() { return mDetail->mProps; }
bool MyBlur::process(frame_container& frames) {
// Apply a simple blur using OpenCV or NPP – omitted for brevity
return true;
}
Step 2 – Register the Module with the Declarative Registry
Registration connects your C++ implementation to the declarative pipeline system. Add your module to base/src/declarative/ModuleRegistrations.cpp inside the ensureBuiltinModulesRegistered() function:
// base/src/declarative/ModuleRegistrations.cpp
// Inside ensureBuiltinModulesRegistered()
if (!registry.hasModule("MyBlur")) {
registerModule<MyBlur, MyBlurProps>()
.category(ModuleCategory::Transform)
.description("Simple Gaussian blur (CPU) with configurable radius")
.tags("transform", "blur", "cpu")
.input("input", "RawImage")
.output("output", "RawImage")
.intProp("radius", "Blur kernel radius (odd number)", true, 3, 1, 31)
.dynamicProp("radius", "int", "Runtime‑adjustable blur radius", false, "3")
.selfManagedOutputPins(); // not needed for static pins but harmless
}
The registration macros (intProp, dynamicProp, etc.) map JavaScript/JSON property names to your C++ Props class fields. The registerModule<> template instantiates the factory that creates your module when pipelines reference the type name "MyBlur".
Step 3 – Use the Module in Pipelines
Once registered and rebuilt, your module is accessible by its type name without additional binding code.
JSON Pipeline Configuration
Reference the module in a JSON pipeline file:
{
"modules": {
"blur": {
"type": "MyBlur",
"props": { "radius": 5 }
}
},
"connections": [
{ "src": "source", "dst": "blur", "srcPin": "output", "dstPin": "input" },
{ "src": "blur", "dst": "sink", "srcPin": "output", "dstPin": "input" }
]
}
Run the pipeline using the CLI:
./build/aprapipes_cli run mypipeline.json
Node.js API Integration
Access the module through the native addon:
const ap = require('./aprapipes.node');
const pipeline = ap.createPipeline({
modules: {
blur: { type: "MyBlur", props: { radius: 5 } }
},
connections: []
});
const blurMod = pipeline.getModule('blur');
console.log('Current radius =', blurMod.getProperty('radius'));
blurMod.setProperty('radius', 9); // Dynamic change while running
Understanding the Registration Architecture
The declarative system uses a centralized registry that maps type names to factory functions. The flow works as follows:
JSON / JS → ModuleRegistry (looks up type name) → ModuleFactory (creates instance)
↑ |
└─ registration metadata (inputs/outputs/props) stored in ModuleInfo
The ensureBuiltinModulesRegistered() function—called automatically by aprapipes_cli.cpp and the Node.js addon.cpp—populates the registry by executing the registration blocks in ModuleRegistrations.cpp. When a pipeline requests a module by type name, the factory uses the stored ModuleInfo to instantiate the class, bind properties via applyProperties, and wire input/output pins.
Key Files Reference
| File | Role | Example Reference |
|---|---|---|
base/include/<YourModule>.h |
Header with *Props and module class definitions |
VirtualPTZ.h |
base/src/<YourModule>.cpp |
Implementation of processing logic and property handling | VirtualPTZ.cpp |
base/src/declarative/ModuleRegistrations.cpp |
Central registration of all built-in modules | ModuleRegistrations.cpp |
base/tools/aprapipes_cli.cpp |
CLI entry point that triggers registration | aprapipes_cli.cpp |
base/bindings/node/addon.cpp |
Node.js addon entry point | addon.cpp |
docs/declarative-pipeline/DEVELOPER_GUIDE.md |
Comprehensive developer documentation | DEVELOPER_GUIDE.md |
Summary
- Define the Props class: Inherit from
ModulePropsand implementapplyProperties(), plus optional dynamic property getters/setters for runtime configuration. - Implement the Module class: Inherit from
Module, implementinit(),term(), andprocess(), and handle property updates viasetProps()andcanQueueProps(). - Register declaratively: Add a
registerModule<>entry inbase/src/declarative/ModuleRegistrations.cppspecifying inputs, outputs, and property metadata. - Rebuild and deploy: Compile with
cmake --buildand use the module immediately in JSON pipelines or Node.js applications without additional binding code.
Frequently Asked Questions
Where should I place the header and source files for a new custom module?
Place the header file in base/include/<YourModule>.h and the implementation in base/src/<YourModule>.cpp. This follows the existing project structure used by reference modules like VirtualPTZ and ensures the build system automatically includes your files when compiling the base library.
How does the declarative registry know about my module's properties?
The registry learns about your properties through the applyProperties template method in your *Props class and the registration macros in ModuleRegistrations.cpp. When you call .intProp() or .dynamicProp() during registration, you map JSON/JavaScript property names to the C++ fields. The ModuleFactory uses this metadata to automatically bind values from pipeline configurations to your class instances via applyProperties().
Can I update module properties while the pipeline is running?
Yes, if you implement dynamic property support. Define dynamicPropertyNames(), getProperty(), and setProperty() in your Props class, and use the .dynamicProp() macro during registration. At runtime, call canQueueProps() to check if updates can be queued, or apply them immediately if the module supports dynamic changes. The Node.js API exposes this through module.getProperty() and module.setProperty() calls.
What is the difference between selfManagedOutputPins() and standard pin management?
Standard pin management allows the framework to automatically create and connect output pins based on the registration metadata. Calling selfManagedOutputPins() indicates that your module manually controls its output pin lifecycle, which is useful for modules that dynamically change their output format based on input or internal state. For most static transforms, you can omit this or include it as a safeguard.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →